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