wininet: Fix a couple of memory leaks.
[wine.git] / programs / xcopy / xcopy.c
blob7ff751c2014d61599812d3030b3ec4a0bfa5ff4d
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*sizeof(WCHAR));
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 fclose(inFile);
295 return TRUE;
298 /* Revert the input string to original form, and cleanup + return */
299 *endOfName = endChar;
300 fclose(inFile);
301 return FALSE;
304 /* =========================================================================
305 * Process the /EXCLUDE: file list, building up a list of substrings to
306 * avoid copying
307 * Returns TRUE on any failure
308 * ========================================================================= */
309 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
311 WCHAR *filenameStart = parms;
313 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
314 excludeList = NULL;
316 while (*parms && *parms != ' ' && *parms != '/') {
318 /* If found '+' then process the file found so far */
319 if (*parms == '+') {
320 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
321 return TRUE;
323 filenameStart = parms+1;
325 parms++;
328 if (filenameStart != parms) {
329 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
330 return TRUE;
334 return FALSE;
337 /* =========================================================================
338 XCOPY_DoCopy - Recursive function to copy files based on input parms
339 of a stem and a spec
341 This works by using FindFirstFile supplying the source stem and spec.
342 If results are found, any non-directory ones are processed
343 Then, if /S or /E is supplied, another search is made just for
344 directories, and this function is called again for that directory
346 ========================================================================= */
347 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
348 WCHAR *deststem, WCHAR *destspec,
349 DWORD flags)
351 WIN32_FIND_DATAW *finddata;
352 HANDLE h;
353 BOOL findres = TRUE;
354 WCHAR *inputpath, *outputpath;
355 BOOL copiedFile = FALSE;
356 DWORD destAttribs, srcAttribs;
357 BOOL skipFile;
358 int ret = 0;
360 /* Allocate some working memory on heap to minimize footprint */
361 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
362 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
363 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
365 /* Build the search info into a single parm */
366 lstrcpyW(inputpath, srcstem);
367 lstrcatW(inputpath, srcspec);
369 /* Search 1 - Look for matching files */
370 h = FindFirstFileW(inputpath, finddata);
371 while (h != INVALID_HANDLE_VALUE && findres) {
373 skipFile = FALSE;
375 /* Ignore . and .. */
376 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
377 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
378 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
380 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
381 } else {
383 /* Get the filename information */
384 lstrcpyW(copyFrom, srcstem);
385 if (flags & OPT_SHORTNAME) {
386 lstrcatW(copyFrom, finddata->cAlternateFileName);
387 } else {
388 lstrcatW(copyFrom, finddata->cFileName);
391 lstrcpyW(copyTo, deststem);
392 if (*destspec == 0x00) {
393 if (flags & OPT_SHORTNAME) {
394 lstrcatW(copyTo, finddata->cAlternateFileName);
395 } else {
396 lstrcatW(copyTo, finddata->cFileName);
398 } else {
399 lstrcatW(copyTo, destspec);
402 /* Do the copy */
403 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
404 wine_dbgstr_w(copyTo));
405 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
407 /* See if allowed to copy it */
408 srcAttribs = GetFileAttributesW(copyFrom);
409 WINE_TRACE("Source attribs: %d\n", srcAttribs);
411 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
412 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
414 if (!(flags & OPT_COPYHIDSYS)) {
415 skipFile = TRUE;
419 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
420 (flags & OPT_ARCHIVEONLY)) {
421 skipFile = TRUE;
424 /* See if file exists */
425 destAttribs = GetFileAttributesW(copyTo);
426 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
428 /* Check date ranges if a destination file already exists */
429 if (!skipFile && (flags & OPT_DATERANGE) &&
430 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
431 WINE_TRACE("Skipping file as modified date too old\n");
432 skipFile = TRUE;
435 /* If just /D supplied, only overwrite if src newer than dest */
436 if (!skipFile && (flags & OPT_DATENEWER) &&
437 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
438 HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
439 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
440 NULL);
441 if (h != INVALID_HANDLE_VALUE) {
442 FILETIME writeTime;
443 GetFileTime(h, NULL, NULL, &writeTime);
445 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
446 WINE_TRACE("Skipping file as dest newer or same date\n");
447 skipFile = TRUE;
449 CloseHandle(h);
453 /* See if exclude list provided. Note since filenames are case
454 insensitive, need to uppercase the filename before doing
455 strstr */
456 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
457 EXCLUDELIST *pos = excludeList;
458 WCHAR copyFromUpper[MAX_PATH];
460 /* Uppercase source filename */
461 lstrcpyW(copyFromUpper, copyFrom);
462 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
464 /* Loop through testing each exclude line */
465 while (pos) {
466 if (wcsstr(copyFromUpper, pos->name) != NULL) {
467 WINE_TRACE("Skipping file as matches exclude '%s'\n",
468 wine_dbgstr_w(pos->name));
469 skipFile = TRUE;
470 pos = NULL;
471 } else {
472 pos = pos->next;
477 /* Prompt each file if necessary */
478 if (!skipFile && (flags & OPT_SRCPROMPT)) {
479 DWORD count;
480 char answer[10];
481 BOOL answered = FALSE;
482 WCHAR yesChar[2];
483 WCHAR noChar[2];
485 /* Read the Y and N characters from the resource file */
486 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
487 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
489 while (!answered) {
490 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
491 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
492 &count, NULL);
494 answered = TRUE;
495 if (toupper(answer[0]) == noChar[0])
496 skipFile = TRUE;
497 else if (toupper(answer[0]) != yesChar[0])
498 answered = FALSE;
502 if (!skipFile &&
503 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
504 DWORD count;
505 char answer[10];
506 BOOL answered = FALSE;
507 WCHAR yesChar[2];
508 WCHAR allChar[2];
509 WCHAR noChar[2];
511 /* Read the A,Y and N characters from the resource file */
512 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
513 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
514 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
516 while (!answered) {
517 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
518 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
519 &count, NULL);
521 answered = TRUE;
522 if (toupper(answer[0]) == allChar[0])
523 flags |= OPT_NOPROMPT;
524 else if (toupper(answer[0]) == noChar[0])
525 skipFile = TRUE;
526 else if (toupper(answer[0]) != yesChar[0])
527 answered = FALSE;
531 /* See if it has to exist! */
532 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
533 skipFile = TRUE;
536 /* Output a status message */
537 if (!skipFile) {
538 if (flags & OPT_QUIET) {
539 /* Skip message */
540 } else if (flags & OPT_FULL) {
541 const WCHAR infostr[] = {'%', '1', ' ', '-', '>', ' ',
542 '%', '2', '\n', 0};
544 XCOPY_wprintf(infostr, copyFrom, copyTo);
545 } else {
546 const WCHAR infostr[] = {'%', '1', '\n', 0};
547 XCOPY_wprintf(infostr, copyFrom);
550 /* If allowing overwriting of read only files, remove any
551 write protection */
552 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
553 (flags & OPT_REPLACEREAD)) {
554 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
557 copiedFile = TRUE;
558 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
559 /* Skip copy */
560 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
562 DWORD error = GetLastError();
563 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
564 copyFrom, copyTo, error);
565 XCOPY_FailMessage(error);
567 if (flags & OPT_IGNOREERRORS) {
568 skipFile = TRUE;
569 } else {
570 ret = RC_WRITEERROR;
571 goto cleanup;
575 /* If /M supplied, remove the archive bit after successful copy */
576 if (!skipFile) {
577 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
578 (flags & OPT_REMOVEARCH)) {
579 SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
581 filesCopied++;
586 /* Find next file */
587 findres = FindNextFileW(h, finddata);
589 FindClose(h);
591 /* Search 2 - do subdirs */
592 if (flags & OPT_RECURSIVE) {
593 lstrcpyW(inputpath, srcstem);
594 lstrcatW(inputpath, wchr_star);
595 findres = TRUE;
596 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
598 h = FindFirstFileW(inputpath, finddata);
599 while (h != INVALID_HANDLE_VALUE && findres) {
601 /* Only looking for dirs */
602 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
603 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
604 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
606 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
608 /* Make up recursive information */
609 lstrcpyW(inputpath, srcstem);
610 lstrcatW(inputpath, finddata->cFileName);
611 lstrcatW(inputpath, wchr_slash);
613 lstrcpyW(outputpath, deststem);
614 if (*destspec == 0x00) {
615 lstrcatW(outputpath, finddata->cFileName);
617 /* If /E is supplied, create the directory now */
618 if ((flags & OPT_EMPTYDIR) &&
619 !(flags & OPT_SIMULATE))
620 XCOPY_CreateDirectory(outputpath);
622 lstrcatW(outputpath, wchr_slash);
625 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
628 /* Find next one */
629 findres = FindNextFileW(h, finddata);
631 FindClose(h);
634 cleanup:
636 /* free up memory */
637 HeapFree(GetProcessHeap(), 0, finddata);
638 HeapFree(GetProcessHeap(), 0, inputpath);
639 HeapFree(GetProcessHeap(), 0, outputpath);
641 return ret;
645 /* =========================================================================
646 XCOPY_ParseCommandLine - Parses the command line
647 ========================================================================= */
648 static BOOL is_whitespace(WCHAR c)
650 return c == ' ' || c == '\t';
653 static WCHAR *skip_whitespace(WCHAR *p)
655 for (; *p && is_whitespace(*p); p++);
656 return p;
659 /* Windows XCOPY uses a simplified command line parsing algorithm
660 that lacks the escaped-quote logic of build_argv(), because
661 literal double quotes are illegal in any of its arguments.
662 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
663 static int find_end_of_word(const WCHAR *word, WCHAR **end)
665 BOOL in_quotes = FALSE;
666 const WCHAR *ptr = word;
667 for (;;) {
668 for (; *ptr != '\0' && *ptr != '"' &&
669 (in_quotes || !is_whitespace(*ptr)); ptr++);
670 if (*ptr == '"') {
671 in_quotes = !in_quotes;
672 ptr++;
674 /* Odd number of double quotes is illegal for XCOPY */
675 if (in_quotes && *ptr == '\0')
676 return RC_INITERROR;
677 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
678 break;
680 *end = (WCHAR*)ptr;
681 return RC_OK;
684 /* Remove all double quotes from a word */
685 static void strip_quotes(WCHAR *word, WCHAR **end)
687 WCHAR *rp, *wp;
688 for (rp = word, wp = word; *rp != '\0'; rp++) {
689 if (*rp == '"')
690 continue;
691 if (wp < rp)
692 *wp = *rp;
693 wp++;
695 *wp = '\0';
696 *end = wp;
699 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
700 WCHAR *supplieddestination, DWORD *pflags)
702 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
703 DWORD flags = *pflags;
704 WCHAR *cmdline, *word, *end, *next;
705 int rc = RC_INITERROR;
707 cmdline = _wcsdup(GetCommandLineW());
708 if (cmdline == NULL)
709 return rc;
711 /* Skip first arg, which is the program name */
712 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
713 goto out;
714 word = skip_whitespace(word);
716 while (*word)
718 WCHAR first;
719 if ((rc = find_end_of_word(word, &end)) != RC_OK)
720 goto out;
722 next = skip_whitespace(end);
723 first = word[0];
724 *end = '\0';
725 strip_quotes(word, &end);
726 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
728 /* First non-switch parameter is source, second is destination */
729 if (first != '/') {
730 if (suppliedsource[0] == 0x00) {
731 lstrcpyW(suppliedsource, word);
732 } else if (supplieddestination[0] == 0x00) {
733 lstrcpyW(supplieddestination, word);
734 } else {
735 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
736 goto out;
738 } else {
739 /* Process all the switch options
740 Note: Windows docs say /P prompts when dest is created
741 but tests show it is done for each src file
742 regardless of the destination */
743 switch (toupper(word[1])) {
744 case 'I': flags |= OPT_ASSUMEDIR; break;
745 case 'S': flags |= OPT_RECURSIVE; break;
746 case 'Q': flags |= OPT_QUIET; break;
747 case 'F': flags |= OPT_FULL; break;
748 case 'L': flags |= OPT_SIMULATE; break;
749 case 'W': flags |= OPT_PAUSE; break;
750 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
751 case 'Y': flags |= OPT_NOPROMPT; break;
752 case 'N': flags |= OPT_SHORTNAME; break;
753 case 'U': flags |= OPT_MUSTEXIST; break;
754 case 'R': flags |= OPT_REPLACEREAD; break;
755 case 'H': flags |= OPT_COPYHIDSYS; break;
756 case 'C': flags |= OPT_IGNOREERRORS; break;
757 case 'P': flags |= OPT_SRCPROMPT; break;
758 case 'A': flags |= OPT_ARCHIVEONLY; break;
759 case 'M': flags |= OPT_ARCHIVEONLY |
760 OPT_REMOVEARCH; break;
762 /* E can be /E or /EXCLUDE */
763 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
764 NORM_IGNORECASE | SORT_STRINGSORT,
765 &word[1], 8,
766 EXCLUDE, -1) == CSTR_EQUAL) {
767 if (XCOPY_ProcessExcludeList(&word[9])) {
768 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
769 goto out;
770 } else flags |= OPT_EXCLUDELIST;
771 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
772 break;
774 /* D can be /D or /D: */
775 case 'D': if (word[2]==':' && isdigit(word[3])) {
776 SYSTEMTIME st;
777 WCHAR *pos = &word[3];
778 BOOL isError = FALSE;
779 memset(&st, 0x00, sizeof(st));
781 /* Microsoft xcopy's usage message implies that the date
782 * format depends on the locale, but that is false.
783 * It is hardcoded to month-day-year.
785 st.wMonth = _wtol(pos);
786 while (*pos && isdigit(*pos)) pos++;
787 if (*pos++ != '-') isError = TRUE;
789 if (!isError) {
790 st.wDay = _wtol(pos);
791 while (*pos && isdigit(*pos)) pos++;
792 if (*pos++ != '-') isError = TRUE;
795 if (!isError) {
796 st.wYear = _wtol(pos);
797 while (*pos && isdigit(*pos)) pos++;
798 if (st.wYear < 100) st.wYear+=2000;
801 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
802 SYSTEMTIME st;
803 WCHAR datestring[32], timestring[32];
805 flags |= OPT_DATERANGE;
807 /* Debug info: */
808 FileTimeToSystemTime (&dateRange, &st);
809 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
810 sizeof(datestring)/sizeof(WCHAR));
811 GetTimeFormatW(0, TIME_NOSECONDS, &st,
812 NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
814 WINE_TRACE("Date being used is: %s %s\n",
815 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
816 } else {
817 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
818 goto out;
820 } else {
821 flags |= OPT_DATENEWER;
823 break;
825 case '-': if (toupper(word[2])=='Y')
826 flags &= ~OPT_NOPROMPT;
827 break;
828 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
829 rc = RC_HELP;
830 goto out;
831 default:
832 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
833 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
834 goto out;
837 word = next;
840 /* Default the destination if not supplied */
841 if (supplieddestination[0] == 0x00)
842 lstrcpyW(supplieddestination, wchr_dot);
844 *pflags = flags;
845 rc = RC_OK;
847 out:
848 free(cmdline);
849 return rc;
853 /* =========================================================================
854 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
855 converts it into a stem and a filespec
856 ========================================================================= */
857 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
858 WCHAR *spec, DWORD flags)
860 WCHAR actualsource[MAX_PATH];
861 WCHAR *starPos;
862 WCHAR *questPos;
863 DWORD attribs;
866 * Validate the source, expanding to full path ensuring it exists
868 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
869 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
870 return RC_INITERROR;
873 /* If full names required, convert to using the full path */
874 if (flags & OPT_FULL) {
875 lstrcpyW(suppliedsource, actualsource);
879 * Work out the stem of the source
882 /* If a directory is supplied, use that as-is (either fully or
883 partially qualified)
884 If a filename is supplied + a directory or drive path, use that
885 as-is
886 Otherwise
887 If no directory or path specified, add eg. C:
888 stem is Drive/Directory is bit up to last \ (or first :)
889 spec is bit after that */
891 starPos = wcschr(suppliedsource, '*');
892 questPos = wcschr(suppliedsource, '?');
893 if (starPos || questPos) {
894 attribs = 0x00; /* Ensures skips invalid or directory check below */
895 } else {
896 attribs = GetFileAttributesW(actualsource);
899 if (attribs == INVALID_FILE_ATTRIBUTES) {
900 XCOPY_FailMessage(GetLastError());
901 return RC_INITERROR;
903 /* Directory:
904 stem should be exactly as supplied plus a '\', unless it was
905 eg. C: in which case no slash required */
906 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
907 WCHAR lastChar;
909 WINE_TRACE("Directory supplied\n");
910 lstrcpyW(stem, suppliedsource);
911 lastChar = stem[lstrlenW(stem)-1];
912 if (lastChar != '\\' && lastChar != ':') {
913 lstrcatW(stem, wchr_slash);
915 lstrcpyW(spec, wchr_star);
917 /* File or wildcard search:
918 stem should be:
919 Up to and including last slash if directory path supplied
920 If c:filename supplied, just the c:
921 Otherwise stem should be the current drive letter + ':' */
922 } else {
923 WCHAR *lastDir;
925 WINE_TRACE("Filename supplied\n");
926 lastDir = wcsrchr(suppliedsource, '\\');
928 if (lastDir) {
929 lstrcpyW(stem, suppliedsource);
930 stem[(lastDir-suppliedsource) + 1] = 0x00;
931 lstrcpyW(spec, (lastDir+1));
932 } else if (suppliedsource[1] == ':') {
933 lstrcpyW(stem, suppliedsource);
934 stem[2] = 0x00;
935 lstrcpyW(spec, suppliedsource+2);
936 } else {
937 WCHAR curdir[MAXSTRING];
938 GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
939 stem[0] = curdir[0];
940 stem[1] = curdir[1];
941 stem[2] = 0x00;
942 lstrcpyW(spec, suppliedsource);
946 return RC_OK;
949 /* =========================================================================
950 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
951 converts it into a stem
952 ========================================================================= */
953 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
954 WCHAR *srcspec, DWORD flags)
956 WCHAR actualdestination[MAX_PATH];
957 DWORD attribs;
958 BOOL isDir = FALSE;
961 * Validate the source, expanding to full path ensuring it exists
963 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
964 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
965 return RC_INITERROR;
968 /* Destination is either a directory or a file */
969 attribs = GetFileAttributesW(actualdestination);
971 if (attribs == INVALID_FILE_ATTRIBUTES) {
973 /* If /I supplied and wildcard copy, assume directory */
974 /* Also if destination ends with backslash */
975 if ((flags & OPT_ASSUMEDIR &&
976 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
977 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
979 isDir = TRUE;
981 } else {
982 DWORD count;
983 char answer[10] = "";
984 WCHAR fileChar[2];
985 WCHAR dirChar[2];
987 /* Read the F and D characters from the resource file */
988 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
989 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
991 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
992 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
994 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
995 WINE_TRACE("User answer %c\n", answer[0]);
997 answer[0] = toupper(answer[0]);
1000 if (answer[0] == dirChar[0]) {
1001 isDir = TRUE;
1002 } else {
1003 isDir = FALSE;
1006 } else {
1007 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1010 if (isDir) {
1011 lstrcpyW(stem, actualdestination);
1012 *spec = 0x00;
1014 /* Ensure ends with a '\' */
1015 if (stem[lstrlenW(stem)-1] != '\\') {
1016 lstrcatW(stem, wchr_slash);
1019 } else {
1020 WCHAR drive[MAX_PATH];
1021 WCHAR dir[MAX_PATH];
1022 WCHAR fname[MAX_PATH];
1023 WCHAR ext[MAX_PATH];
1024 _wsplitpath(actualdestination, drive, dir, fname, ext);
1025 lstrcpyW(stem, drive);
1026 lstrcatW(stem, dir);
1027 lstrcpyW(spec, fname);
1028 lstrcatW(spec, ext);
1030 return RC_OK;
1034 /* =========================================================================
1035 main - Main entrypoint for the xcopy command
1037 Processes the args, and drives the actual copying
1038 ========================================================================= */
1039 int wmain (int argc, WCHAR *argvW[])
1041 int rc = 0;
1042 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1043 WCHAR supplieddestination[MAX_PATH] = {0};
1044 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1045 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1046 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1047 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1048 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1049 DWORD flags = 0; /* Option flags */
1050 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
1051 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
1052 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1054 /* Preinitialize flags based on COPYCMD */
1055 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1056 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1057 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1058 flags |= OPT_NOPROMPT;
1062 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1063 wine, but on windows these can be normal files. At least one installer
1064 uses files such as .packlist and (validly) expects them to be copied.
1065 Under wine, if we do not copy hidden files by default then they get
1066 lose */
1067 flags |= OPT_COPYHIDSYS;
1070 * Parse the command line
1072 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1073 &flags)) != RC_OK) {
1074 if (rc == RC_HELP)
1075 return RC_OK;
1076 else
1077 return rc;
1080 /* Trace out the supplied information */
1081 WINE_TRACE("Supplied parameters:\n");
1082 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1083 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1085 /* Extract required information from source specification */
1086 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1087 if (rc != RC_OK) return rc;
1089 /* Extract required information from destination specification */
1090 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1091 destinationspec, sourcespec, flags);
1092 if (rc != RC_OK) return rc;
1094 /* Trace out the resulting information */
1095 WINE_TRACE("Resolved parameters:\n");
1096 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1097 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1098 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1099 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1101 /* Pause if necessary */
1102 if (flags & OPT_PAUSE) {
1103 DWORD count;
1104 char pausestr[10];
1106 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1107 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1108 &count, NULL);
1111 /* Now do the hard work... */
1112 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1113 destinationstem, destinationspec,
1114 flags);
1116 /* Clear up exclude list allocated memory */
1117 while (excludeList) {
1118 EXCLUDELIST *pos = excludeList;
1119 excludeList = excludeList -> next;
1120 HeapFree(GetProcessHeap(), 0, pos->name);
1121 HeapFree(GetProcessHeap(), 0, pos);
1124 /* Finished - print trailer and exit */
1125 if (flags & OPT_SIMULATE) {
1126 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1127 } else if (!(flags & OPT_NOCOPY)) {
1128 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1130 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
1131 return rc;