libport: Add a replacement implementation for strnlen.
[wine.git] / programs / xcopy / xcopy.c
bloba173cc14c7b2a423bbf2b7d329400024bccdf1d7
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 WINAPIV 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 /* If more than CRLF */
275 if (length > 1) {
276 buffer[length-1] = 0; /* strip CRLF */
277 thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
278 thisEntry->next = excludeList;
279 excludeList = thisEntry;
280 thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
281 (length * sizeof(WCHAR))+1);
282 lstrcpyW(thisEntry->name, buffer);
283 CharUpperBuffW(thisEntry->name, length);
284 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
288 /* See if EOF or error occurred */
289 if (!feof(inFile)) {
290 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
291 *endOfName = endChar;
292 fclose(inFile);
293 return TRUE;
296 /* Revert the input string to original form, and cleanup + return */
297 *endOfName = endChar;
298 fclose(inFile);
299 return FALSE;
302 /* =========================================================================
303 * Process the /EXCLUDE: file list, building up a list of substrings to
304 * avoid copying
305 * Returns TRUE on any failure
306 * ========================================================================= */
307 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
309 WCHAR *filenameStart = parms;
311 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
312 excludeList = NULL;
314 while (*parms && *parms != ' ' && *parms != '/') {
316 /* If found '+' then process the file found so far */
317 if (*parms == '+') {
318 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
319 return TRUE;
321 filenameStart = parms+1;
323 parms++;
326 if (filenameStart != parms) {
327 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
328 return TRUE;
332 return FALSE;
335 /* =========================================================================
336 XCOPY_DoCopy - Recursive function to copy files based on input parms
337 of a stem and a spec
339 This works by using FindFirstFile supplying the source stem and spec.
340 If results are found, any non-directory ones are processed
341 Then, if /S or /E is supplied, another search is made just for
342 directories, and this function is called again for that directory
344 ========================================================================= */
345 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
346 WCHAR *deststem, WCHAR *destspec,
347 DWORD flags)
349 WIN32_FIND_DATAW *finddata;
350 HANDLE h;
351 BOOL findres = TRUE;
352 WCHAR *inputpath, *outputpath;
353 BOOL copiedFile = FALSE;
354 DWORD destAttribs, srcAttribs;
355 BOOL skipFile;
356 int ret = 0;
358 /* Allocate some working memory on heap to minimize footprint */
359 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
360 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
361 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
363 /* Build the search info into a single parm */
364 lstrcpyW(inputpath, srcstem);
365 lstrcatW(inputpath, srcspec);
367 /* Search 1 - Look for matching files */
368 h = FindFirstFileW(inputpath, finddata);
369 while (h != INVALID_HANDLE_VALUE && findres) {
371 skipFile = FALSE;
373 /* Ignore . and .. */
374 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
375 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
376 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
378 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
379 } else {
381 /* Get the filename information */
382 lstrcpyW(copyFrom, srcstem);
383 if (flags & OPT_SHORTNAME) {
384 lstrcatW(copyFrom, finddata->cAlternateFileName);
385 } else {
386 lstrcatW(copyFrom, finddata->cFileName);
389 lstrcpyW(copyTo, deststem);
390 if (*destspec == 0x00) {
391 if (flags & OPT_SHORTNAME) {
392 lstrcatW(copyTo, finddata->cAlternateFileName);
393 } else {
394 lstrcatW(copyTo, finddata->cFileName);
396 } else {
397 lstrcatW(copyTo, destspec);
400 /* Do the copy */
401 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
402 wine_dbgstr_w(copyTo));
403 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
405 /* See if allowed to copy it */
406 srcAttribs = GetFileAttributesW(copyFrom);
407 WINE_TRACE("Source attribs: %d\n", srcAttribs);
409 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
410 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
412 if (!(flags & OPT_COPYHIDSYS)) {
413 skipFile = TRUE;
417 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
418 (flags & OPT_ARCHIVEONLY)) {
419 skipFile = TRUE;
422 /* See if file exists */
423 destAttribs = GetFileAttributesW(copyTo);
424 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
426 /* Check date ranges if a destination file already exists */
427 if (!skipFile && (flags & OPT_DATERANGE) &&
428 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
429 WINE_TRACE("Skipping file as modified date too old\n");
430 skipFile = TRUE;
433 /* If just /D supplied, only overwrite if src newer than dest */
434 if (!skipFile && (flags & OPT_DATENEWER) &&
435 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
436 HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
437 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
438 NULL);
439 if (h != INVALID_HANDLE_VALUE) {
440 FILETIME writeTime;
441 GetFileTime(h, NULL, NULL, &writeTime);
443 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
444 WINE_TRACE("Skipping file as dest newer or same date\n");
445 skipFile = TRUE;
447 CloseHandle(h);
451 /* See if exclude list provided. Note since filenames are case
452 insensitive, need to uppercase the filename before doing
453 strstr */
454 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
455 EXCLUDELIST *pos = excludeList;
456 WCHAR copyFromUpper[MAX_PATH];
458 /* Uppercase source filename */
459 lstrcpyW(copyFromUpper, copyFrom);
460 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
462 /* Loop through testing each exclude line */
463 while (pos) {
464 if (wcsstr(copyFromUpper, pos->name) != NULL) {
465 WINE_TRACE("Skipping file as matches exclude '%s'\n",
466 wine_dbgstr_w(pos->name));
467 skipFile = TRUE;
468 pos = NULL;
469 } else {
470 pos = pos->next;
475 /* Prompt each file if necessary */
476 if (!skipFile && (flags & OPT_SRCPROMPT)) {
477 DWORD count;
478 char answer[10];
479 BOOL answered = FALSE;
480 WCHAR yesChar[2];
481 WCHAR noChar[2];
483 /* Read the Y and N characters from the resource file */
484 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
485 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
487 while (!answered) {
488 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
489 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
490 &count, NULL);
492 answered = TRUE;
493 if (toupper(answer[0]) == noChar[0])
494 skipFile = TRUE;
495 else if (toupper(answer[0]) != yesChar[0])
496 answered = FALSE;
500 if (!skipFile &&
501 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
502 DWORD count;
503 char answer[10];
504 BOOL answered = FALSE;
505 WCHAR yesChar[2];
506 WCHAR allChar[2];
507 WCHAR noChar[2];
509 /* Read the A,Y and N characters from the resource file */
510 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
511 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
512 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
514 while (!answered) {
515 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
516 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
517 &count, NULL);
519 answered = TRUE;
520 if (toupper(answer[0]) == allChar[0])
521 flags |= OPT_NOPROMPT;
522 else if (toupper(answer[0]) == noChar[0])
523 skipFile = TRUE;
524 else if (toupper(answer[0]) != yesChar[0])
525 answered = FALSE;
529 /* See if it has to exist! */
530 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
531 skipFile = TRUE;
534 /* Output a status message */
535 if (!skipFile) {
536 if (flags & OPT_QUIET) {
537 /* Skip message */
538 } else if (flags & OPT_FULL) {
539 const WCHAR infostr[] = {'%', '1', ' ', '-', '>', ' ',
540 '%', '2', '\n', 0};
542 XCOPY_wprintf(infostr, copyFrom, copyTo);
543 } else {
544 const WCHAR infostr[] = {'%', '1', '\n', 0};
545 XCOPY_wprintf(infostr, copyFrom);
548 /* If allowing overwriting of read only files, remove any
549 write protection */
550 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
551 (flags & OPT_REPLACEREAD)) {
552 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
555 copiedFile = TRUE;
556 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
557 /* Skip copy */
558 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
560 DWORD error = GetLastError();
561 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
562 copyFrom, copyTo, error);
563 XCOPY_FailMessage(error);
565 if (flags & OPT_IGNOREERRORS) {
566 skipFile = TRUE;
567 } else {
568 ret = RC_WRITEERROR;
569 goto cleanup;
573 /* If /M supplied, remove the archive bit after successful copy */
574 if (!skipFile) {
575 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
576 (flags & OPT_REMOVEARCH)) {
577 SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
579 filesCopied++;
584 /* Find next file */
585 findres = FindNextFileW(h, finddata);
587 FindClose(h);
589 /* Search 2 - do subdirs */
590 if (flags & OPT_RECURSIVE) {
591 lstrcpyW(inputpath, srcstem);
592 lstrcatW(inputpath, wchr_star);
593 findres = TRUE;
594 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
596 h = FindFirstFileW(inputpath, finddata);
597 while (h != INVALID_HANDLE_VALUE && findres) {
599 /* Only looking for dirs */
600 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
601 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
602 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
604 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
606 /* Make up recursive information */
607 lstrcpyW(inputpath, srcstem);
608 lstrcatW(inputpath, finddata->cFileName);
609 lstrcatW(inputpath, wchr_slash);
611 lstrcpyW(outputpath, deststem);
612 if (*destspec == 0x00) {
613 lstrcatW(outputpath, finddata->cFileName);
615 /* If /E is supplied, create the directory now */
616 if ((flags & OPT_EMPTYDIR) &&
617 !(flags & OPT_SIMULATE))
618 XCOPY_CreateDirectory(outputpath);
620 lstrcatW(outputpath, wchr_slash);
623 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
626 /* Find next one */
627 findres = FindNextFileW(h, finddata);
629 FindClose(h);
632 cleanup:
634 /* free up memory */
635 HeapFree(GetProcessHeap(), 0, finddata);
636 HeapFree(GetProcessHeap(), 0, inputpath);
637 HeapFree(GetProcessHeap(), 0, outputpath);
639 return ret;
643 /* =========================================================================
644 XCOPY_ParseCommandLine - Parses the command line
645 ========================================================================= */
646 static inline BOOL is_whitespace(WCHAR c)
648 return c == ' ' || c == '\t';
651 static WCHAR *skip_whitespace(WCHAR *p)
653 for (; *p && is_whitespace(*p); p++);
654 return p;
657 static inline BOOL is_digit(WCHAR c)
659 return c >= '0' && c <= '9';
662 /* Windows XCOPY uses a simplified command line parsing algorithm
663 that lacks the escaped-quote logic of build_argv(), because
664 literal double quotes are illegal in any of its arguments.
665 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
666 static int find_end_of_word(const WCHAR *word, WCHAR **end)
668 BOOL in_quotes = FALSE;
669 const WCHAR *ptr = word;
670 for (;;) {
671 for (; *ptr != '\0' && *ptr != '"' &&
672 (in_quotes || !is_whitespace(*ptr)); ptr++);
673 if (*ptr == '"') {
674 in_quotes = !in_quotes;
675 ptr++;
677 /* Odd number of double quotes is illegal for XCOPY */
678 if (in_quotes && *ptr == '\0')
679 return RC_INITERROR;
680 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
681 break;
683 *end = (WCHAR*)ptr;
684 return RC_OK;
687 /* Remove all double quotes from a word */
688 static void strip_quotes(WCHAR *word, WCHAR **end)
690 WCHAR *rp, *wp;
691 for (rp = word, wp = word; *rp != '\0'; rp++) {
692 if (*rp == '"')
693 continue;
694 if (wp < rp)
695 *wp = *rp;
696 wp++;
698 *wp = '\0';
699 *end = wp;
702 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
703 WCHAR *supplieddestination, DWORD *pflags)
705 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
706 DWORD flags = *pflags;
707 WCHAR *cmdline, *word, *end, *next;
708 int rc = RC_INITERROR;
710 cmdline = _wcsdup(GetCommandLineW());
711 if (cmdline == NULL)
712 return rc;
714 /* Skip first arg, which is the program name */
715 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
716 goto out;
717 word = skip_whitespace(word);
719 while (*word)
721 WCHAR first;
722 if ((rc = find_end_of_word(word, &end)) != RC_OK)
723 goto out;
725 next = skip_whitespace(end);
726 first = word[0];
727 *end = '\0';
728 strip_quotes(word, &end);
729 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
731 /* First non-switch parameter is source, second is destination */
732 if (first != '/') {
733 if (suppliedsource[0] == 0x00) {
734 lstrcpyW(suppliedsource, word);
735 } else if (supplieddestination[0] == 0x00) {
736 lstrcpyW(supplieddestination, word);
737 } else {
738 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
739 goto out;
741 } else {
742 /* Process all the switch options
743 Note: Windows docs say /P prompts when dest is created
744 but tests show it is done for each src file
745 regardless of the destination */
746 switch (toupper(word[1])) {
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 'H': flags |= OPT_COPYHIDSYS; break;
759 case 'C': flags |= OPT_IGNOREERRORS; break;
760 case 'P': flags |= OPT_SRCPROMPT; break;
761 case 'A': flags |= OPT_ARCHIVEONLY; break;
762 case 'M': flags |= OPT_ARCHIVEONLY |
763 OPT_REMOVEARCH; break;
765 /* E can be /E or /EXCLUDE */
766 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
767 NORM_IGNORECASE | SORT_STRINGSORT,
768 &word[1], 8,
769 EXCLUDE, -1) == CSTR_EQUAL) {
770 if (XCOPY_ProcessExcludeList(&word[9])) {
771 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
772 goto out;
773 } else flags |= OPT_EXCLUDELIST;
774 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
775 break;
777 /* D can be /D or /D: */
778 case 'D': if (word[2]==':' && is_digit(word[3])) {
779 SYSTEMTIME st;
780 WCHAR *pos = &word[3];
781 BOOL isError = FALSE;
782 memset(&st, 0x00, sizeof(st));
784 /* Microsoft xcopy's usage message implies that the date
785 * format depends on the locale, but that is false.
786 * It is hardcoded to month-day-year.
788 st.wMonth = _wtol(pos);
789 while (*pos && is_digit(*pos)) pos++;
790 if (*pos++ != '-') isError = TRUE;
792 if (!isError) {
793 st.wDay = _wtol(pos);
794 while (*pos && is_digit(*pos)) pos++;
795 if (*pos++ != '-') isError = TRUE;
798 if (!isError) {
799 st.wYear = _wtol(pos);
800 while (*pos && is_digit(*pos)) pos++;
801 if (st.wYear < 100) st.wYear+=2000;
804 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
805 SYSTEMTIME st;
806 WCHAR datestring[32], timestring[32];
808 flags |= OPT_DATERANGE;
810 /* Debug info: */
811 FileTimeToSystemTime (&dateRange, &st);
812 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
813 sizeof(datestring)/sizeof(WCHAR));
814 GetTimeFormatW(0, TIME_NOSECONDS, &st,
815 NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
817 WINE_TRACE("Date being used is: %s %s\n",
818 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
819 } else {
820 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
821 goto out;
823 } else {
824 flags |= OPT_DATENEWER;
826 break;
828 case '-': if (toupper(word[2])=='Y')
829 flags &= ~OPT_NOPROMPT;
830 break;
831 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
832 rc = RC_HELP;
833 goto out;
834 case 'V':
835 WINE_FIXME("ignoring /V\n");
836 break;
837 default:
838 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
839 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
840 goto out;
843 word = next;
846 /* Default the destination if not supplied */
847 if (supplieddestination[0] == 0x00)
848 lstrcpyW(supplieddestination, wchr_dot);
850 *pflags = flags;
851 rc = RC_OK;
853 out:
854 free(cmdline);
855 return rc;
859 /* =========================================================================
860 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
861 converts it into a stem and a filespec
862 ========================================================================= */
863 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
864 WCHAR *spec, DWORD flags)
866 WCHAR actualsource[MAX_PATH];
867 WCHAR *starPos;
868 WCHAR *questPos;
869 DWORD attribs;
872 * Validate the source, expanding to full path ensuring it exists
874 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
875 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
876 return RC_INITERROR;
879 /* If full names required, convert to using the full path */
880 if (flags & OPT_FULL) {
881 lstrcpyW(suppliedsource, actualsource);
885 * Work out the stem of the source
888 /* If a directory is supplied, use that as-is (either fully or
889 partially qualified)
890 If a filename is supplied + a directory or drive path, use that
891 as-is
892 Otherwise
893 If no directory or path specified, add eg. C:
894 stem is Drive/Directory is bit up to last \ (or first :)
895 spec is bit after that */
897 starPos = wcschr(suppliedsource, '*');
898 questPos = wcschr(suppliedsource, '?');
899 if (starPos || questPos) {
900 attribs = 0x00; /* Ensures skips invalid or directory check below */
901 } else {
902 attribs = GetFileAttributesW(actualsource);
905 if (attribs == INVALID_FILE_ATTRIBUTES) {
906 XCOPY_FailMessage(GetLastError());
907 return RC_INITERROR;
909 /* Directory:
910 stem should be exactly as supplied plus a '\', unless it was
911 eg. C: in which case no slash required */
912 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
913 WCHAR lastChar;
915 WINE_TRACE("Directory supplied\n");
916 lstrcpyW(stem, suppliedsource);
917 lastChar = stem[lstrlenW(stem)-1];
918 if (lastChar != '\\' && lastChar != ':') {
919 lstrcatW(stem, wchr_slash);
921 lstrcpyW(spec, wchr_star);
923 /* File or wildcard search:
924 stem should be:
925 Up to and including last slash if directory path supplied
926 If c:filename supplied, just the c:
927 Otherwise stem should be the current drive letter + ':' */
928 } else {
929 WCHAR *lastDir;
931 WINE_TRACE("Filename supplied\n");
932 lastDir = wcsrchr(suppliedsource, '\\');
934 if (lastDir) {
935 lstrcpyW(stem, suppliedsource);
936 stem[(lastDir-suppliedsource) + 1] = 0x00;
937 lstrcpyW(spec, (lastDir+1));
938 } else if (suppliedsource[1] == ':') {
939 lstrcpyW(stem, suppliedsource);
940 stem[2] = 0x00;
941 lstrcpyW(spec, suppliedsource+2);
942 } else {
943 WCHAR curdir[MAXSTRING];
944 GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
945 stem[0] = curdir[0];
946 stem[1] = curdir[1];
947 stem[2] = 0x00;
948 lstrcpyW(spec, suppliedsource);
952 return RC_OK;
955 /* =========================================================================
956 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
957 converts it into a stem
958 ========================================================================= */
959 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
960 WCHAR *srcspec, DWORD flags)
962 WCHAR actualdestination[MAX_PATH];
963 DWORD attribs;
964 BOOL isDir = FALSE;
967 * Validate the source, expanding to full path ensuring it exists
969 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
970 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
971 return RC_INITERROR;
974 /* Destination is either a directory or a file */
975 attribs = GetFileAttributesW(actualdestination);
977 if (attribs == INVALID_FILE_ATTRIBUTES) {
979 /* If /I supplied and wildcard copy, assume directory */
980 /* Also if destination ends with backslash */
981 if ((flags & OPT_ASSUMEDIR &&
982 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
983 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
985 isDir = TRUE;
987 } else {
988 DWORD count;
989 char answer[10] = "";
990 WCHAR fileChar[2];
991 WCHAR dirChar[2];
993 /* Read the F and D characters from the resource file */
994 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
995 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
997 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
998 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
1000 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
1001 WINE_TRACE("User answer %c\n", answer[0]);
1003 answer[0] = toupper(answer[0]);
1006 if (answer[0] == dirChar[0]) {
1007 isDir = TRUE;
1008 } else {
1009 isDir = FALSE;
1012 } else {
1013 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1016 if (isDir) {
1017 lstrcpyW(stem, actualdestination);
1018 *spec = 0x00;
1020 /* Ensure ends with a '\' */
1021 if (stem[lstrlenW(stem)-1] != '\\') {
1022 lstrcatW(stem, wchr_slash);
1025 } else {
1026 WCHAR drive[MAX_PATH];
1027 WCHAR dir[MAX_PATH];
1028 WCHAR fname[MAX_PATH];
1029 WCHAR ext[MAX_PATH];
1030 _wsplitpath(actualdestination, drive, dir, fname, ext);
1031 lstrcpyW(stem, drive);
1032 lstrcatW(stem, dir);
1033 lstrcpyW(spec, fname);
1034 lstrcatW(spec, ext);
1036 return RC_OK;
1040 /* =========================================================================
1041 main - Main entrypoint for the xcopy command
1043 Processes the args, and drives the actual copying
1044 ========================================================================= */
1045 int wmain (int argc, WCHAR *argvW[])
1047 int rc = 0;
1048 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1049 WCHAR supplieddestination[MAX_PATH] = {0};
1050 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1051 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1052 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1053 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1054 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1055 DWORD flags = 0; /* Option flags */
1056 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
1057 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
1058 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1060 /* Preinitialize flags based on COPYCMD */
1061 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1062 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1063 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1064 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 if (rc == RC_HELP)
1081 return RC_OK;
1082 else
1083 return rc;
1086 /* Trace out the supplied information */
1087 WINE_TRACE("Supplied parameters:\n");
1088 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1089 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1091 /* Extract required information from source specification */
1092 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1093 if (rc != RC_OK) return rc;
1095 /* Extract required information from destination specification */
1096 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1097 destinationspec, sourcespec, flags);
1098 if (rc != RC_OK) return rc;
1100 /* Trace out the resulting information */
1101 WINE_TRACE("Resolved parameters:\n");
1102 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1103 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1104 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1105 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1107 /* Pause if necessary */
1108 if (flags & OPT_PAUSE) {
1109 DWORD count;
1110 char pausestr[10];
1112 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1113 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1114 &count, NULL);
1117 /* Now do the hard work... */
1118 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1119 destinationstem, destinationspec,
1120 flags);
1122 /* Clear up exclude list allocated memory */
1123 while (excludeList) {
1124 EXCLUDELIST *pos = excludeList;
1125 excludeList = excludeList -> next;
1126 HeapFree(GetProcessHeap(), 0, pos->name);
1127 HeapFree(GetProcessHeap(), 0, pos);
1130 /* Finished - print trailer and exit */
1131 if (flags & OPT_SIMULATE) {
1132 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1133 } else if (!(flags & OPT_NOCOPY)) {
1134 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1136 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
1137 return rc;