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