[XCOPY]
[reactos.git] / reactos / base / applications / cmdutils / xcopy / xcopy.c
1 /*
2 * XCOPY - Wine-compatible xcopy program
3 *
4 * Copyright (C) 2007 J. Edmeades
5 *
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.
10 *
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.
15 *
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
19 */
20
21 /*
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
30 */
31
32 /*
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
40 */
41
42
43 #include <stdio.h>
44 #include <windows.h>
45 #include <wine/debug.h>
46 #include <wine/unicode.h>
47 #include "xcopy.h"
48
49 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
50
51
52 /* Typedefs */
53 typedef struct _EXCLUDELIST
54 {
55 struct _EXCLUDELIST *next;
56 WCHAR *name;
57 } EXCLUDELIST;
58
59
60 /* Global variables */
61 static ULONG filesCopied = 0; /* Number of files copied */
62 static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */
63 static FILETIME dateRange; /* Date range to copy after*/
64 static const WCHAR wchr_slash[] = {'\\', 0};
65 static const WCHAR wchr_star[] = {'*', 0};
66 static const WCHAR wchr_dot[] = {'.', 0};
67 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
68
69
70 /* To minimize stack usage during recursion, some temporary variables
71 made global */
72 static WCHAR copyFrom[MAX_PATH];
73 static WCHAR copyTo[MAX_PATH];
74
75
76 /* =========================================================================
77 * Load a string from the resource file, handling any error
78 * Returns string retrieved from resource file
79 * ========================================================================= */
80 static WCHAR *XCOPY_LoadMessage(UINT id) {
81 static WCHAR msg[MAXSTRING];
82 const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
83
84 if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) {
85 WINE_FIXME("LoadString failed with %d\n", GetLastError());
86 lstrcpyW(msg, failedMsg);
87 }
88 return msg;
89 }
90
91 /* =========================================================================
92 * Output a formatted unicode string. Ideally this will go to the console
93 * and hence required WriteConsoleW to output it, however if file i/o is
94 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
95 * ========================================================================= */
96 static int __cdecl XCOPY_wprintf(const WCHAR *format, ...) {
97
98 static WCHAR *output_bufW = NULL;
99 static char *output_bufA = NULL;
100 static BOOL toConsole = TRUE;
101 static BOOL traceOutput = FALSE;
102 #define MAX_WRITECONSOLE_SIZE 65535
103
104 __ms_va_list parms;
105 DWORD nOut;
106 int len;
107 DWORD res = 0;
108
109 /*
110 * Allocate buffer to use when writing to console
111 * Note: Not freed - memory will be allocated once and released when
112 * xcopy ends
113 */
114
115 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
116 MAX_WRITECONSOLE_SIZE);
117 if (!output_bufW) {
118 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
119 return 0;
120 }
121
122 __ms_va_start(parms, format);
123 SetLastError(NO_ERROR);
124 len = FormatMessageW(FORMAT_MESSAGE_FROM_STRING, format, 0, 0, output_bufW,
125 MAX_WRITECONSOLE_SIZE/sizeof(*output_bufW), &parms);
126 __ms_va_end(parms);
127 if (len == 0 && GetLastError() != NO_ERROR) {
128 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format));
129 return 0;
130 }
131
132 /* Try to write as unicode whenever we think it's a console */
133 if (toConsole) {
134 res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
135 output_bufW, len, &nOut, NULL);
136 }
137
138 /* If writing to console has failed (ever) we assume it's file
139 i/o so convert to OEM codepage and output */
140 if (!res) {
141 BOOL usedDefaultChar = FALSE;
142 DWORD convertedChars;
143
144 toConsole = FALSE;
145
146 /*
147 * Allocate buffer to use when writing to file. Not freed, as above
148 */
149 if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
150 MAX_WRITECONSOLE_SIZE);
151 if (!output_bufA) {
152 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
153 return 0;
154 }
155
156 /* Convert to OEM, then output */
157 convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
158 len, output_bufA, MAX_WRITECONSOLE_SIZE,
159 "?", &usedDefaultChar);
160 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
161 &nOut, FALSE);
162 }
163
164 /* Trace whether screen or console */
165 if (!traceOutput) {
166 WINE_TRACE("Writing to console? (%d)\n", toConsole);
167 traceOutput = TRUE;
168 }
169 return nOut;
170 }
171
172 /* =========================================================================
173 * Load a string for a system error and writes it to the screen
174 * Returns string retrieved from resource file
175 * ========================================================================= */
176 static void XCOPY_FailMessage(DWORD err) {
177 LPWSTR lpMsgBuf;
178 int status;
179
180 status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
181 FORMAT_MESSAGE_FROM_SYSTEM,
182 NULL, err, 0,
183 (LPWSTR) &lpMsgBuf, 0, NULL);
184 if (!status) {
185 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
186 err, GetLastError());
187 } else {
188 const WCHAR infostr[] = {'%', '1', '\n', 0};
189 XCOPY_wprintf(infostr, lpMsgBuf);
190 LocalFree ((HLOCAL)lpMsgBuf);
191 }
192 }
193
194
195 /* =========================================================================
196 * Routine copied from cmd.exe md command -
197 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
198 * dir2 if they do not already exist.
199 * ========================================================================= */
200 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
201 {
202 int len;
203 WCHAR *new_path;
204 BOOL ret = TRUE;
205
206 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
207 lstrcpyW(new_path,path);
208
209 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
210 new_path[len - 1] = 0;
211
212 while (!CreateDirectoryW(new_path,NULL))
213 {
214 WCHAR *slash;
215 DWORD last_error = GetLastError();
216 if (last_error == ERROR_ALREADY_EXISTS)
217 break;
218
219 if (last_error != ERROR_PATH_NOT_FOUND)
220 {
221 ret = FALSE;
222 break;
223 }
224
225 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
226 {
227 ret = FALSE;
228 break;
229 }
230
231 len = slash - new_path;
232 new_path[len] = 0;
233 if (!XCOPY_CreateDirectory(new_path))
234 {
235 ret = FALSE;
236 break;
237 }
238 new_path[len] = '\\';
239 }
240 HeapFree(GetProcessHeap(),0,new_path);
241 return ret;
242 }
243
244 /* =========================================================================
245 * Process a single file from the /EXCLUDE: file list, building up a list
246 * of substrings to avoid copying
247 * Returns TRUE on any failure
248 * ========================================================================= */
249 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
250
251 WCHAR endChar = *endOfName;
252 WCHAR buffer[MAXSTRING];
253 FILE *inFile = NULL;
254 const WCHAR readTextMode[] = {'r', 't', 0};
255
256 /* Null terminate the filename (temporarily updates the filename hence
257 parms not const) */
258 *endOfName = 0x00;
259
260 /* Open the file */
261 inFile = _wfopen(filename, readTextMode);
262 if (inFile == NULL) {
263 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
264 *endOfName = endChar;
265 return TRUE;
266 }
267
268 /* Process line by line */
269 while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) {
270 EXCLUDELIST *thisEntry;
271 int length = lstrlenW(buffer);
272
273 /* Strip CRLF */
274 buffer[length-1] = 0x00;
275
276 /* If more than CRLF */
277 if (length > 1) {
278 thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
279 thisEntry->next = excludeList;
280 excludeList = thisEntry;
281 thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
282 (length * sizeof(WCHAR))+1);
283 lstrcpyW(thisEntry->name, buffer);
284 CharUpperBuffW(thisEntry->name, length);
285 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
286 }
287 }
288
289 /* See if EOF or error occurred */
290 if (!feof(inFile)) {
291 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
292 *endOfName = endChar;
293 return TRUE;
294 }
295
296 /* Revert the input string to original form, and cleanup + return */
297 *endOfName = endChar;
298 fclose(inFile);
299 return FALSE;
300 }
301
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) {
308
309 WCHAR *filenameStart = parms;
310
311 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
312 excludeList = NULL;
313
314 while (*parms && *parms != ' ' && *parms != '/') {
315
316 /* If found '+' then process the file found so far */
317 if (*parms == '+') {
318 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
319 return TRUE;
320 }
321 filenameStart = parms+1;
322 }
323 parms++;
324 }
325
326 if (filenameStart != parms) {
327 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
328 return TRUE;
329 }
330 }
331
332 return FALSE;
333 }
334
335 /* =========================================================================
336 XCOPY_DoCopy - Recursive function to copy files based on input parms
337 of a stem and a spec
338
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
343
344 ========================================================================= */
345 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
346 WCHAR *deststem, WCHAR *destspec,
347 DWORD flags)
348 {
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;
357
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));
362
363 /* Build the search info into a single parm */
364 lstrcpyW(inputpath, srcstem);
365 lstrcatW(inputpath, srcspec);
366
367 /* Search 1 - Look for matching files */
368 h = FindFirstFileW(inputpath, finddata);
369 while (h != INVALID_HANDLE_VALUE && findres) {
370
371 skipFile = FALSE;
372
373 /* Ignore . and .. */
374 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
375 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
376 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
377
378 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
379 } else {
380
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);
387 }
388
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);
395 }
396 } else {
397 lstrcatW(copyTo, destspec);
398 }
399
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);
404
405 /* See if allowed to copy it */
406 srcAttribs = GetFileAttributesW(copyFrom);
407 WINE_TRACE("Source attribs: %d\n", srcAttribs);
408
409 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
410 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
411
412 if (!(flags & OPT_COPYHIDSYS)) {
413 skipFile = TRUE;
414 }
415 }
416
417 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
418 (flags & OPT_ARCHIVEONLY)) {
419 skipFile = TRUE;
420 }
421
422 /* See if file exists */
423 destAttribs = GetFileAttributesW(copyTo);
424 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
425
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;
431 }
432
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);
442
443 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
444 WINE_TRACE("Skipping file as dest newer or same date\n");
445 skipFile = TRUE;
446 }
447 CloseHandle(h);
448 }
449 }
450
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];
457
458 /* Uppercase source filename */
459 lstrcpyW(copyFromUpper, copyFrom);
460 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
461
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;
471 }
472 }
473 }
474
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];
482
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));
486
487 while (!answered) {
488 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
489 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
490 &count, NULL);
491
492 answered = TRUE;
493 if (toupper(answer[0]) == noChar[0])
494 skipFile = TRUE;
495 else if (toupper(answer[0]) != yesChar[0])
496 answered = FALSE;
497 }
498 }
499
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];
508
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));
513
514 while (!answered) {
515 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
516 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
517 &count, NULL);
518
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;
526 }
527 }
528
529 /* See if it has to exist! */
530 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
531 skipFile = TRUE;
532 }
533
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};
541
542 XCOPY_wprintf(infostr, copyFrom, copyTo);
543 } else {
544 const WCHAR infostr[] = {'%', '1', '\n', 0};
545 XCOPY_wprintf(infostr, copyFrom);
546 }
547
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);
553 }
554
555 copiedFile = TRUE;
556 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
557 /* Skip copy */
558 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
559
560 DWORD error = GetLastError();
561 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
562 copyFrom, copyTo, error);
563 XCOPY_FailMessage(error);
564
565 if (flags & OPT_IGNOREERRORS) {
566 skipFile = TRUE;
567 } else {
568 ret = RC_WRITEERROR;
569 goto cleanup;
570 }
571 }
572
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));
578 }
579 filesCopied++;
580 }
581 }
582 }
583
584 /* Find next file */
585 findres = FindNextFileW(h, finddata);
586 }
587 FindClose(h);
588
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));
595
596 h = FindFirstFileW(inputpath, finddata);
597 while (h != INVALID_HANDLE_VALUE && findres) {
598
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)) {
603
604 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
605
606 /* Make up recursive information */
607 lstrcpyW(inputpath, srcstem);
608 lstrcatW(inputpath, finddata->cFileName);
609 lstrcatW(inputpath, wchr_slash);
610
611 lstrcpyW(outputpath, deststem);
612 if (*destspec == 0x00) {
613 lstrcatW(outputpath, finddata->cFileName);
614
615 /* If /E is supplied, create the directory now */
616 if ((flags & OPT_EMPTYDIR) &&
617 !(flags & OPT_SIMULATE))
618 XCOPY_CreateDirectory(outputpath);
619
620 lstrcatW(outputpath, wchr_slash);
621 }
622
623 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
624 }
625
626 /* Find next one */
627 findres = FindNextFileW(h, finddata);
628 }
629 }
630
631 cleanup:
632
633 /* free up memory */
634 HeapFree(GetProcessHeap(), 0, finddata);
635 HeapFree(GetProcessHeap(), 0, inputpath);
636 HeapFree(GetProcessHeap(), 0, outputpath);
637
638 return ret;
639 }
640
641
642 /* =========================================================================
643 XCOPY_ParseCommandLine - Parses the command line
644 ========================================================================= */
645 static BOOL is_whitespace(WCHAR c)
646 {
647 return c == ' ' || c == '\t';
648 }
649
650 static WCHAR *skip_whitespace(WCHAR *p)
651 {
652 for (; *p && is_whitespace(*p); p++);
653 return p;
654 }
655
656 /* Windows XCOPY uses a simplified command line parsing algorithm
657 that lacks the escaped-quote logic of build_argv(), because
658 literal double quotes are illegal in any of its arguments.
659 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
660 static int find_end_of_word(const WCHAR *word, WCHAR **end)
661 {
662 BOOL in_quotes = 0;
663 const WCHAR *ptr = word;
664 for (;;) {
665 for (; *ptr != '\0' && *ptr != '"' &&
666 (in_quotes || !is_whitespace(*ptr)); ptr++);
667 if (*ptr == '"') {
668 in_quotes = !in_quotes;
669 ptr++;
670 }
671 /* Odd number of double quotes is illegal for XCOPY */
672 if (in_quotes && *ptr == '\0')
673 return RC_INITERROR;
674 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
675 break;
676 }
677 *end = (WCHAR*)ptr;
678 return RC_OK;
679 }
680
681 /* Remove all double quotes from a word */
682 static void strip_quotes(WCHAR *word, WCHAR **end)
683 {
684 WCHAR *rp, *wp;
685 for (rp = word, wp = word; *rp != '\0'; rp++) {
686 if (*rp == '"')
687 continue;
688 if (wp < rp)
689 *wp = *rp;
690 wp++;
691 }
692 *wp = '\0';
693 *end = wp;
694 }
695
696 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
697 WCHAR *supplieddestination, DWORD *pflags)
698 {
699 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
700 DWORD flags = *pflags;
701 WCHAR *cmdline, *word, *end, *next;
702 int rc = RC_INITERROR;
703
704 cmdline = _wcsdup(GetCommandLineW());
705 if (cmdline == NULL)
706 return rc;
707
708 /* Skip first arg, which is the program name */
709 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
710 goto out;
711 word = skip_whitespace(word);
712
713 while (*word)
714 {
715 WCHAR first;
716 if ((rc = find_end_of_word(word, &end)) != RC_OK)
717 goto out;
718
719 next = skip_whitespace(end);
720 first = word[0];
721 *end = '\0';
722 strip_quotes(word, &end);
723 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
724
725 /* First non-switch parameter is source, second is destination */
726 if (first != '/') {
727 if (suppliedsource[0] == 0x00) {
728 lstrcpyW(suppliedsource, word);
729 } else if (supplieddestination[0] == 0x00) {
730 lstrcpyW(supplieddestination, word);
731 } else {
732 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
733 goto out;
734 }
735 } else {
736 /* Process all the switch options
737 Note: Windows docs say /P prompts when dest is created
738 but tests show it is done for each src file
739 regardless of the destination */
740 switch (toupper(word[1])) {
741 case 'I': flags |= OPT_ASSUMEDIR; break;
742 case 'S': flags |= OPT_RECURSIVE; break;
743 case 'Q': flags |= OPT_QUIET; break;
744 case 'F': flags |= OPT_FULL; break;
745 case 'L': flags |= OPT_SIMULATE; break;
746 case 'W': flags |= OPT_PAUSE; break;
747 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
748 case 'Y': flags |= OPT_NOPROMPT; break;
749 case 'N': flags |= OPT_SHORTNAME; break;
750 case 'U': flags |= OPT_MUSTEXIST; break;
751 case 'R': flags |= OPT_REPLACEREAD; break;
752 case 'H': flags |= OPT_COPYHIDSYS; break;
753 case 'C': flags |= OPT_IGNOREERRORS; break;
754 case 'P': flags |= OPT_SRCPROMPT; break;
755 case 'A': flags |= OPT_ARCHIVEONLY; break;
756 case 'M': flags |= OPT_ARCHIVEONLY |
757 OPT_REMOVEARCH; break;
758
759 /* E can be /E or /EXCLUDE */
760 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
761 NORM_IGNORECASE | SORT_STRINGSORT,
762 &word[1], 8,
763 EXCLUDE, -1) == 2) {
764 if (XCOPY_ProcessExcludeList(&word[9])) {
765 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
766 goto out;
767 } else flags |= OPT_EXCLUDELIST;
768 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
769 break;
770
771 /* D can be /D or /D: */
772 case 'D': if (word[2]==':' && isdigit(word[3])) {
773 SYSTEMTIME st;
774 WCHAR *pos = &word[3];
775 BOOL isError = FALSE;
776 memset(&st, 0x00, sizeof(st));
777
778 /* Parse the arg : Month */
779 st.wMonth = _wtol(pos);
780 while (*pos && isdigit(*pos)) pos++;
781 if (*pos++ != '-') isError = TRUE;
782
783 /* Parse the arg : Day */
784 if (!isError) {
785 st.wDay = _wtol(pos);
786 while (*pos && isdigit(*pos)) pos++;
787 if (*pos++ != '-') isError = TRUE;
788 }
789
790 /* Parse the arg : Year */
791 if (!isError) {
792 st.wYear = _wtol(pos);
793 while (*pos && isdigit(*pos)) pos++;
794 if (st.wYear < 100) st.wYear+=2000;
795 }
796
797 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
798 SYSTEMTIME st;
799 WCHAR datestring[32], timestring[32];
800
801 flags |= OPT_DATERANGE;
802
803 /* Debug info: */
804 FileTimeToSystemTime (&dateRange, &st);
805 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
806 sizeof(datestring)/sizeof(WCHAR));
807 GetTimeFormatW(0, TIME_NOSECONDS, &st,
808 NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
809
810 WINE_TRACE("Date being used is: %s %s\n",
811 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
812 } else {
813 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
814 goto out;
815 }
816 } else {
817 flags |= OPT_DATENEWER;
818 }
819 break;
820
821 case '-': if (toupper(word[2])=='Y')
822 flags &= ~OPT_NOPROMPT; break;
823 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
824 rc = RC_HELP;
825 goto out;
826 default:
827 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
828 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
829 goto out;
830 }
831 }
832 word = next;
833 }
834
835 /* Default the destination if not supplied */
836 if (supplieddestination[0] == 0x00)
837 lstrcpyW(supplieddestination, wchr_dot);
838
839 *pflags = flags;
840 rc = RC_OK;
841
842 out:
843 free(cmdline);
844 return rc;
845 }
846
847
848 /* =========================================================================
849 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
850 converts it into a stem and a filespec
851 ========================================================================= */
852 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
853 WCHAR *spec, DWORD flags)
854 {
855 WCHAR actualsource[MAX_PATH];
856 WCHAR *starPos;
857 WCHAR *questPos;
858 DWORD attribs;
859
860 /*
861 * Validate the source, expanding to full path ensuring it exists
862 */
863 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
864 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
865 return RC_INITERROR;
866 }
867
868 /* If full names required, convert to using the full path */
869 if (flags & OPT_FULL) {
870 lstrcpyW(suppliedsource, actualsource);
871 }
872
873 /*
874 * Work out the stem of the source
875 */
876
877 /* If a directory is supplied, use that as-is (either fully or
878 partially qualified)
879 If a filename is supplied + a directory or drive path, use that
880 as-is
881 Otherwise
882 If no directory or path specified, add eg. C:
883 stem is Drive/Directory is bit up to last \ (or first :)
884 spec is bit after that */
885
886 starPos = wcschr(suppliedsource, '*');
887 questPos = wcschr(suppliedsource, '?');
888 if (starPos || questPos) {
889 attribs = 0x00; /* Ensures skips invalid or directory check below */
890 } else {
891 attribs = GetFileAttributesW(actualsource);
892 }
893
894 if (attribs == INVALID_FILE_ATTRIBUTES) {
895 XCOPY_FailMessage(GetLastError());
896 return RC_INITERROR;
897
898 /* Directory:
899 stem should be exactly as supplied plus a '\', unless it was
900 eg. C: in which case no slash required */
901 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
902 WCHAR lastChar;
903
904 WINE_TRACE("Directory supplied\n");
905 lstrcpyW(stem, suppliedsource);
906 lastChar = stem[lstrlenW(stem)-1];
907 if (lastChar != '\\' && lastChar != ':') {
908 lstrcatW(stem, wchr_slash);
909 }
910 lstrcpyW(spec, wchr_star);
911
912 /* File or wildcard search:
913 stem should be:
914 Up to and including last slash if directory path supplied
915 If c:filename supplied, just the c:
916 Otherwise stem should be the current drive letter + ':' */
917 } else {
918 WCHAR *lastDir;
919
920 WINE_TRACE("Filename supplied\n");
921 lastDir = wcsrchr(suppliedsource, '\\');
922
923 if (lastDir) {
924 lstrcpyW(stem, suppliedsource);
925 stem[(lastDir-suppliedsource) + 1] = 0x00;
926 lstrcpyW(spec, (lastDir+1));
927 } else if (suppliedsource[1] == ':') {
928 lstrcpyW(stem, suppliedsource);
929 stem[2] = 0x00;
930 lstrcpyW(spec, suppliedsource+2);
931 } else {
932 WCHAR curdir[MAXSTRING];
933 GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
934 stem[0] = curdir[0];
935 stem[1] = curdir[1];
936 stem[2] = 0x00;
937 lstrcpyW(spec, suppliedsource);
938 }
939 }
940
941 return RC_OK;
942 }
943
944 /* =========================================================================
945 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
946 converts it into a stem
947 ========================================================================= */
948 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
949 WCHAR *srcspec, DWORD flags)
950 {
951 WCHAR actualdestination[MAX_PATH];
952 DWORD attribs;
953 BOOL isDir = FALSE;
954
955 /*
956 * Validate the source, expanding to full path ensuring it exists
957 */
958 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
959 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
960 return RC_INITERROR;
961 }
962
963 /* Destination is either a directory or a file */
964 attribs = GetFileAttributesW(actualdestination);
965
966 if (attribs == INVALID_FILE_ATTRIBUTES) {
967
968 /* If /I supplied and wildcard copy, assume directory */
969 /* Also if destination ends with backslash */
970 if ((flags & OPT_ASSUMEDIR &&
971 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
972 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
973
974 isDir = TRUE;
975
976 } else {
977 DWORD count;
978 char answer[10] = "";
979 WCHAR fileChar[2];
980 WCHAR dirChar[2];
981
982 /* Read the F and D characters from the resource file */
983 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
984 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
985
986 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
987 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
988
989 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
990 WINE_TRACE("User answer %c\n", answer[0]);
991
992 answer[0] = toupper(answer[0]);
993 }
994
995 if (answer[0] == dirChar[0]) {
996 isDir = TRUE;
997 } else {
998 isDir = FALSE;
999 }
1000 }
1001 } else {
1002 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1003 }
1004
1005 if (isDir) {
1006 lstrcpyW(stem, actualdestination);
1007 *spec = 0x00;
1008
1009 /* Ensure ends with a '\' */
1010 if (stem[lstrlenW(stem)-1] != '\\') {
1011 lstrcatW(stem, wchr_slash);
1012 }
1013
1014 } else {
1015 WCHAR drive[MAX_PATH];
1016 WCHAR dir[MAX_PATH];
1017 WCHAR fname[MAX_PATH];
1018 WCHAR ext[MAX_PATH];
1019 _wsplitpath(actualdestination, drive, dir, fname, ext);
1020 lstrcpyW(stem, drive);
1021 lstrcatW(stem, dir);
1022 lstrcpyW(spec, fname);
1023 lstrcatW(spec, ext);
1024 }
1025 return RC_OK;
1026 }
1027
1028
1029 /* =========================================================================
1030 main - Main entrypoint for the xcopy command
1031
1032 Processes the args, and drives the actual copying
1033 ========================================================================= */
1034 int wmain (int argc, WCHAR *argvW[])
1035 {
1036 int rc = 0;
1037 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1038 WCHAR supplieddestination[MAX_PATH] = {0};
1039 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1040 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1041 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1042 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1043 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1044 DWORD flags = 0; /* Option flags */
1045 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
1046 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
1047 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1048
1049 /* Preinitialize flags based on COPYCMD */
1050 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1051 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1052 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1053 flags |= OPT_NOPROMPT;
1054 }
1055 }
1056
1057 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1058 wine, but on windows these can be normal files. At least one installer
1059 uses files such as .packlist and (validly) expects them to be copied.
1060 Under wine, if we do not copy hidden files by default then they get
1061 lose */
1062 flags |= OPT_COPYHIDSYS;
1063
1064 /*
1065 * Parse the command line
1066 */
1067 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1068 &flags)) != RC_OK) {
1069 if (rc == RC_HELP)
1070 return RC_OK;
1071 else
1072 return rc;
1073 }
1074
1075 /* Trace out the supplied information */
1076 WINE_TRACE("Supplied parameters:\n");
1077 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1078 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1079
1080 /* Extract required information from source specification */
1081 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1082 if (rc != RC_OK) return rc;
1083
1084 /* Extract required information from destination specification */
1085 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1086 destinationspec, sourcespec, flags);
1087 if (rc != RC_OK) return rc;
1088
1089 /* Trace out the resulting information */
1090 WINE_TRACE("Resolved parameters:\n");
1091 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1092 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1093 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1094 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1095
1096 /* Pause if necessary */
1097 if (flags & OPT_PAUSE) {
1098 DWORD count;
1099 char pausestr[10];
1100
1101 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1102 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1103 &count, NULL);
1104 }
1105
1106 /* Now do the hard work... */
1107 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1108 destinationstem, destinationspec,
1109 flags);
1110
1111 /* Clear up exclude list allocated memory */
1112 while (excludeList) {
1113 EXCLUDELIST *pos = excludeList;
1114 excludeList = excludeList -> next;
1115 HeapFree(GetProcessHeap(), 0, pos->name);
1116 HeapFree(GetProcessHeap(), 0, pos);
1117 }
1118
1119 /* Finished - print trailer and exit */
1120 if (flags & OPT_SIMULATE) {
1121 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1122 } else if (!(flags & OPT_NOCOPY)) {
1123 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1124 }
1125 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
1126 return rc;
1127
1128 }