shlwapi: Convert string table resources to po files.
[wine.git] / programs / xcopy / xcopy.c
blobdaf269b99b79f32e2816751342272b3b2d09fc17
1 /*
2 * XCOPY - Wine-compatible xcopy program
4 * Copyright (C) 2007 J. Edmeades
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
22 * FIXME:
23 * This should now support all options listed in the xcopy help from
24 * windows XP except:
25 * /Z - Copy from network drives in restartable mode
26 * /X - Copy file audit settings (sets /O)
27 * /O - Copy file ownership + ACL info
28 * /G - Copy encrypted files to unencrypted destination
29 * /V - Verifies files
33 * Notes:
34 * Apparently, valid return codes are:
35 * 0 - OK
36 * 1 - No files found to copy
37 * 2 - CTRL+C during copy
38 * 4 - Initialization error, or invalid source specification
39 * 5 - Disk write error
43 #include <stdio.h>
44 #include <windows.h>
45 #include <wine/debug.h>
46 #include <wine/unicode.h>
47 #include "xcopy.h"
49 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
51 /* Prototypes */
52 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
53 WCHAR *supplieddestination, DWORD *flags);
54 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
55 WCHAR *spec, DWORD flags);
56 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
57 WCHAR *spec, WCHAR *srcspec, DWORD flags);
58 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
59 WCHAR *deststem, WCHAR *destspec,
60 DWORD flags);
61 static BOOL XCOPY_CreateDirectory(const WCHAR* path);
62 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms);
63 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName);
64 static WCHAR *XCOPY_LoadMessage(UINT id);
65 static void XCOPY_FailMessage(DWORD err);
66 static int XCOPY_wprintf(const WCHAR *format, ...);
68 /* Typedefs */
69 typedef struct _EXCLUDELIST
71 struct _EXCLUDELIST *next;
72 WCHAR *name;
73 } EXCLUDELIST;
76 /* Global variables */
77 static ULONG filesCopied = 0; /* Number of files copied */
78 static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */
79 static FILETIME dateRange; /* Date range to copy after*/
80 static const WCHAR wchr_slash[] = {'\\', 0};
81 static const WCHAR wchr_star[] = {'*', 0};
82 static const WCHAR wchr_dot[] = {'.', 0};
83 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
85 /* Constants (Mostly for widechars) */
88 /* To minimize stack usage during recursion, some temporary variables
89 made global */
90 static WCHAR copyFrom[MAX_PATH];
91 static WCHAR copyTo[MAX_PATH];
94 /* =========================================================================
95 main - Main entrypoint for the xcopy command
97 Processes the args, and drives the actual copying
98 ========================================================================= */
99 int wmain (int argc, WCHAR *argvW[])
101 int rc = 0;
102 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
103 WCHAR supplieddestination[MAX_PATH] = {0};
104 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
105 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
106 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
107 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
108 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
109 DWORD flags = 0; /* Option flags */
110 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
111 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
112 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
114 /* Preinitialize flags based on COPYCMD */
115 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
116 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
117 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
118 flags |= OPT_NOPROMPT;
122 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
123 wine, but on windows these can be normal files. At least one installer
124 uses files such as .packlist and (validly) expects them to be copied.
125 Under wine, if we do not copy hidden files by default then they get
126 lose */
127 flags |= OPT_COPYHIDSYS;
130 * Parse the command line
132 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
133 &flags)) != RC_OK) {
134 if (rc == RC_HELP)
135 return RC_OK;
136 else
137 return rc;
140 /* Trace out the supplied information */
141 WINE_TRACE("Supplied parameters:\n");
142 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
143 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
145 /* Extract required information from source specification */
146 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
147 if (rc != RC_OK) return rc;
149 /* Extract required information from destination specification */
150 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
151 destinationspec, sourcespec, flags);
152 if (rc != RC_OK) return rc;
154 /* Trace out the resulting information */
155 WINE_TRACE("Resolved parameters:\n");
156 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
157 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
158 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
159 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
161 /* Pause if necessary */
162 if (flags & OPT_PAUSE) {
163 DWORD count;
164 char pausestr[10];
166 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
167 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
168 &count, NULL);
171 /* Now do the hard work... */
172 rc = XCOPY_DoCopy(sourcestem, sourcespec,
173 destinationstem, destinationspec,
174 flags);
176 /* Clear up exclude list allocated memory */
177 while (excludeList) {
178 EXCLUDELIST *pos = excludeList;
179 excludeList = excludeList -> next;
180 HeapFree(GetProcessHeap(), 0, pos->name);
181 HeapFree(GetProcessHeap(), 0, pos);
184 /* Finished - print trailer and exit */
185 if (flags & OPT_SIMULATE) {
186 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
187 } else if (!(flags & OPT_NOCOPY)) {
188 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
190 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
191 return rc;
195 /* =========================================================================
196 XCOPY_ParseCommandLine - Parses the command line
197 ========================================================================= */
198 static BOOL is_whitespace(WCHAR c)
200 return c == ' ' || c == '\t';
203 static WCHAR *skip_whitespace(WCHAR *p)
205 for (; *p && is_whitespace(*p); p++);
206 return p;
209 /* Windows XCOPY uses a simplified command line parsing algorithm
210 that lacks the escaped-quote logic of build_argv(), because
211 literal double quotes are illegal in any of its arguments.
212 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
213 static int find_end_of_word(const WCHAR *word, WCHAR **end)
215 BOOL in_quotes = 0;
216 const WCHAR *ptr = word;
217 for (;;) {
218 for (; *ptr != '\0' && *ptr != '"' &&
219 (in_quotes || !is_whitespace(*ptr)); ptr++);
220 if (*ptr == '"') {
221 in_quotes = !in_quotes;
222 ptr++;
224 /* Odd number of double quotes is illegal for XCOPY */
225 if (in_quotes && *ptr == '\0')
226 return RC_INITERROR;
227 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
228 break;
230 *end = (WCHAR*)ptr;
231 return RC_OK;
234 /* Remove all double quotes from a word */
235 static void strip_quotes(WCHAR *word, WCHAR **end)
237 WCHAR *rp, *wp;
238 for (rp = word, wp = word; *rp != '\0'; rp++) {
239 if (*rp == '"')
240 continue;
241 if (wp < rp)
242 *wp = *rp;
243 wp++;
245 *wp = '\0';
246 *end = wp;
249 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
250 WCHAR *supplieddestination, DWORD *pflags)
252 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
253 DWORD flags = *pflags;
254 WCHAR *cmdline, *word, *end, *next;
255 int rc = RC_INITERROR;
257 cmdline = _wcsdup(GetCommandLineW());
258 if (cmdline == NULL)
259 return rc;
261 /* Skip first arg, which is the program name */
262 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
263 goto out;
264 word = skip_whitespace(word);
266 while (*word)
268 WCHAR first;
269 if ((rc = find_end_of_word(word, &end)) != RC_OK)
270 goto out;
272 next = skip_whitespace(end);
273 first = word[0];
274 *end = '\0';
275 strip_quotes(word, &end);
276 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
278 /* First non-switch parameter is source, second is destination */
279 if (first != '/') {
280 if (suppliedsource[0] == 0x00) {
281 lstrcpyW(suppliedsource, word);
282 } else if (supplieddestination[0] == 0x00) {
283 lstrcpyW(supplieddestination, word);
284 } else {
285 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
286 goto out;
288 } else {
289 /* Process all the switch options
290 Note: Windows docs say /P prompts when dest is created
291 but tests show it is done for each src file
292 regardless of the destination */
293 switch (toupper(word[1])) {
294 case 'I': flags |= OPT_ASSUMEDIR; break;
295 case 'S': flags |= OPT_RECURSIVE; break;
296 case 'Q': flags |= OPT_QUIET; break;
297 case 'F': flags |= OPT_FULL; break;
298 case 'L': flags |= OPT_SIMULATE; break;
299 case 'W': flags |= OPT_PAUSE; break;
300 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
301 case 'Y': flags |= OPT_NOPROMPT; break;
302 case 'N': flags |= OPT_SHORTNAME; break;
303 case 'U': flags |= OPT_MUSTEXIST; break;
304 case 'R': flags |= OPT_REPLACEREAD; break;
305 case 'H': flags |= OPT_COPYHIDSYS; break;
306 case 'C': flags |= OPT_IGNOREERRORS; break;
307 case 'P': flags |= OPT_SRCPROMPT; break;
308 case 'A': flags |= OPT_ARCHIVEONLY; break;
309 case 'M': flags |= OPT_ARCHIVEONLY |
310 OPT_REMOVEARCH; break;
312 /* E can be /E or /EXCLUDE */
313 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
314 NORM_IGNORECASE | SORT_STRINGSORT,
315 &word[1], 8,
316 EXCLUDE, -1) == 2) {
317 if (XCOPY_ProcessExcludeList(&word[9])) {
318 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
319 goto out;
320 } else flags |= OPT_EXCLUDELIST;
321 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
322 break;
324 /* D can be /D or /D: */
325 case 'D': if (word[2]==':' && isdigit(word[3])) {
326 SYSTEMTIME st;
327 WCHAR *pos = &word[3];
328 BOOL isError = FALSE;
329 memset(&st, 0x00, sizeof(st));
331 /* Parse the arg : Month */
332 st.wMonth = _wtol(pos);
333 while (*pos && isdigit(*pos)) pos++;
334 if (*pos++ != '-') isError = TRUE;
336 /* Parse the arg : Day */
337 if (!isError) {
338 st.wDay = _wtol(pos);
339 while (*pos && isdigit(*pos)) pos++;
340 if (*pos++ != '-') isError = TRUE;
343 /* Parse the arg : Day */
344 if (!isError) {
345 st.wYear = _wtol(pos);
346 while (*pos && isdigit(*pos)) pos++;
347 if (st.wYear < 100) st.wYear+=2000;
350 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
351 SYSTEMTIME st;
352 WCHAR datestring[32], timestring[32];
354 flags |= OPT_DATERANGE;
356 /* Debug info: */
357 FileTimeToSystemTime (&dateRange, &st);
358 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
359 sizeof(datestring)/sizeof(WCHAR));
360 GetTimeFormatW(0, TIME_NOSECONDS, &st,
361 NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
363 WINE_TRACE("Date being used is: %s %s\n",
364 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
365 } else {
366 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
367 goto out;
369 } else {
370 flags |= OPT_DATENEWER;
372 break;
374 case '-': if (toupper(word[2])=='Y')
375 flags &= ~OPT_NOPROMPT; break;
376 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
377 rc = RC_HELP;
378 goto out;
379 default:
380 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
381 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
382 goto out;
385 word = next;
388 /* Default the destination if not supplied */
389 if (supplieddestination[0] == 0x00)
390 lstrcpyW(supplieddestination, wchr_dot);
392 *pflags = flags;
393 rc = RC_OK;
395 out:
396 free(cmdline);
397 return rc;
401 /* =========================================================================
402 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
403 converts it into a stem and a filespec
404 ========================================================================= */
405 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
406 WCHAR *spec, DWORD flags)
408 WCHAR actualsource[MAX_PATH];
409 WCHAR *starPos;
410 WCHAR *questPos;
411 DWORD attribs;
414 * Validate the source, expanding to full path ensuring it exists
416 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
417 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
418 return RC_INITERROR;
421 /* If full names required, convert to using the full path */
422 if (flags & OPT_FULL) {
423 lstrcpyW(suppliedsource, actualsource);
427 * Work out the stem of the source
430 /* If a directory is supplied, use that as-is (either fully or
431 partially qualified)
432 If a filename is supplied + a directory or drive path, use that
433 as-is
434 Otherwise
435 If no directory or path specified, add eg. C:
436 stem is Drive/Directory is bit up to last \ (or first :)
437 spec is bit after that */
439 starPos = wcschr(suppliedsource, '*');
440 questPos = wcschr(suppliedsource, '?');
441 if (starPos || questPos) {
442 attribs = 0x00; /* Ensures skips invalid or directory check below */
443 } else {
444 attribs = GetFileAttributesW(actualsource);
447 if (attribs == INVALID_FILE_ATTRIBUTES) {
448 XCOPY_FailMessage(GetLastError());
449 return RC_INITERROR;
451 /* Directory:
452 stem should be exactly as supplied plus a '\', unless it was
453 eg. C: in which case no slash required */
454 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
455 WCHAR lastChar;
457 WINE_TRACE("Directory supplied\n");
458 lstrcpyW(stem, suppliedsource);
459 lastChar = stem[lstrlenW(stem)-1];
460 if (lastChar != '\\' && lastChar != ':') {
461 lstrcatW(stem, wchr_slash);
463 lstrcpyW(spec, wchr_star);
465 /* File or wildcard search:
466 stem should be:
467 Up to and including last slash if directory path supplied
468 If c:filename supplied, just the c:
469 Otherwise stem should be the current drive letter + ':' */
470 } else {
471 WCHAR *lastDir;
473 WINE_TRACE("Filename supplied\n");
474 lastDir = wcsrchr(suppliedsource, '\\');
476 if (lastDir) {
477 lstrcpyW(stem, suppliedsource);
478 stem[(lastDir-suppliedsource) + 1] = 0x00;
479 lstrcpyW(spec, (lastDir+1));
480 } else if (suppliedsource[1] == ':') {
481 lstrcpyW(stem, suppliedsource);
482 stem[2] = 0x00;
483 lstrcpyW(spec, suppliedsource+2);
484 } else {
485 WCHAR curdir[MAXSTRING];
486 GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
487 stem[0] = curdir[0];
488 stem[1] = curdir[1];
489 stem[2] = 0x00;
490 lstrcpyW(spec, suppliedsource);
494 return RC_OK;
497 /* =========================================================================
498 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
499 converts it into a stem
500 ========================================================================= */
501 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
502 WCHAR *srcspec, DWORD flags)
504 WCHAR actualdestination[MAX_PATH];
505 DWORD attribs;
506 BOOL isDir = FALSE;
509 * Validate the source, expanding to full path ensuring it exists
511 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
512 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
513 return RC_INITERROR;
516 /* Destination is either a directory or a file */
517 attribs = GetFileAttributesW(actualdestination);
519 if (attribs == INVALID_FILE_ATTRIBUTES) {
521 /* If /I supplied and wildcard copy, assume directory */
522 /* Also if destination ends with backslash */
523 if ((flags & OPT_ASSUMEDIR &&
524 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
525 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
527 isDir = TRUE;
529 } else {
530 DWORD count;
531 char answer[10] = "";
532 WCHAR fileChar[2];
533 WCHAR dirChar[2];
535 /* Read the F and D characters from the resource file */
536 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
537 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
539 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
540 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
542 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
543 WINE_TRACE("User answer %c\n", answer[0]);
545 answer[0] = toupper(answer[0]);
548 if (answer[0] == dirChar[0]) {
549 isDir = TRUE;
550 } else {
551 isDir = FALSE;
554 } else {
555 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
558 if (isDir) {
559 lstrcpyW(stem, actualdestination);
560 *spec = 0x00;
562 /* Ensure ends with a '\' */
563 if (stem[lstrlenW(stem)-1] != '\\') {
564 lstrcatW(stem, wchr_slash);
567 } else {
568 WCHAR drive[MAX_PATH];
569 WCHAR dir[MAX_PATH];
570 WCHAR fname[MAX_PATH];
571 WCHAR ext[MAX_PATH];
572 _wsplitpath(actualdestination, drive, dir, fname, ext);
573 lstrcpyW(stem, drive);
574 lstrcatW(stem, dir);
575 lstrcpyW(spec, fname);
576 lstrcatW(spec, ext);
578 return RC_OK;
581 /* =========================================================================
582 XCOPY_DoCopy - Recursive function to copy files based on input parms
583 of a stem and a spec
585 This works by using FindFirstFile supplying the source stem and spec.
586 If results are found, any non-directory ones are processed
587 Then, if /S or /E is supplied, another search is made just for
588 directories, and this function is called again for that directory
590 ========================================================================= */
591 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
592 WCHAR *deststem, WCHAR *destspec,
593 DWORD flags)
595 WIN32_FIND_DATAW *finddata;
596 HANDLE h;
597 BOOL findres = TRUE;
598 WCHAR *inputpath, *outputpath;
599 BOOL copiedFile = FALSE;
600 DWORD destAttribs, srcAttribs;
601 BOOL skipFile;
602 int ret = 0;
604 /* Allocate some working memory on heap to minimize footprint */
605 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
606 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
607 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
609 /* Build the search info into a single parm */
610 lstrcpyW(inputpath, srcstem);
611 lstrcatW(inputpath, srcspec);
613 /* Search 1 - Look for matching files */
614 h = FindFirstFileW(inputpath, finddata);
615 while (h != INVALID_HANDLE_VALUE && findres) {
617 skipFile = FALSE;
619 /* Ignore . and .. */
620 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
621 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
622 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
624 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
625 } else {
627 /* Get the filename information */
628 lstrcpyW(copyFrom, srcstem);
629 if (flags & OPT_SHORTNAME) {
630 lstrcatW(copyFrom, finddata->cAlternateFileName);
631 } else {
632 lstrcatW(copyFrom, finddata->cFileName);
635 lstrcpyW(copyTo, deststem);
636 if (*destspec == 0x00) {
637 if (flags & OPT_SHORTNAME) {
638 lstrcatW(copyTo, finddata->cAlternateFileName);
639 } else {
640 lstrcatW(copyTo, finddata->cFileName);
642 } else {
643 lstrcatW(copyTo, destspec);
646 /* Do the copy */
647 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
648 wine_dbgstr_w(copyTo));
649 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
651 /* See if allowed to copy it */
652 srcAttribs = GetFileAttributesW(copyFrom);
653 WINE_TRACE("Source attribs: %d\n", srcAttribs);
655 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
656 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
658 if (!(flags & OPT_COPYHIDSYS)) {
659 skipFile = TRUE;
663 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
664 (flags & OPT_ARCHIVEONLY)) {
665 skipFile = TRUE;
668 /* See if file exists */
669 destAttribs = GetFileAttributesW(copyTo);
670 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
672 /* Check date ranges if a destination file already exists */
673 if (!skipFile && (flags & OPT_DATERANGE) &&
674 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
675 WINE_TRACE("Skipping file as modified date too old\n");
676 skipFile = TRUE;
679 /* If just /D supplied, only overwrite if src newer than dest */
680 if (!skipFile && (flags & OPT_DATENEWER) &&
681 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
682 HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
683 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
684 NULL);
685 if (h != INVALID_HANDLE_VALUE) {
686 FILETIME writeTime;
687 GetFileTime(h, NULL, NULL, &writeTime);
689 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
690 WINE_TRACE("Skipping file as dest newer or same date\n");
691 skipFile = TRUE;
693 CloseHandle(h);
697 /* See if exclude list provided. Note since filenames are case
698 insensitive, need to uppercase the filename before doing
699 strstr */
700 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
701 EXCLUDELIST *pos = excludeList;
702 WCHAR copyFromUpper[MAX_PATH];
704 /* Uppercase source filename */
705 lstrcpyW(copyFromUpper, copyFrom);
706 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
708 /* Loop through testing each exclude line */
709 while (pos) {
710 if (wcsstr(copyFromUpper, pos->name) != NULL) {
711 WINE_TRACE("Skipping file as matches exclude '%s'\n",
712 wine_dbgstr_w(pos->name));
713 skipFile = TRUE;
714 pos = NULL;
715 } else {
716 pos = pos->next;
721 /* Prompt each file if necessary */
722 if (!skipFile && (flags & OPT_SRCPROMPT)) {
723 DWORD count;
724 char answer[10];
725 BOOL answered = FALSE;
726 WCHAR yesChar[2];
727 WCHAR noChar[2];
729 /* Read the Y and N characters from the resource file */
730 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
731 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
733 while (!answered) {
734 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
735 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
736 &count, NULL);
738 answered = TRUE;
739 if (toupper(answer[0]) == noChar[0])
740 skipFile = TRUE;
741 else if (toupper(answer[0]) != yesChar[0])
742 answered = FALSE;
746 if (!skipFile &&
747 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
748 DWORD count;
749 char answer[10];
750 BOOL answered = FALSE;
751 WCHAR yesChar[2];
752 WCHAR allChar[2];
753 WCHAR noChar[2];
755 /* Read the A,Y and N characters from the resource file */
756 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
757 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
758 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
760 while (!answered) {
761 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
762 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
763 &count, NULL);
765 answered = TRUE;
766 if (toupper(answer[0]) == allChar[0])
767 flags |= OPT_NOPROMPT;
768 else if (toupper(answer[0]) == noChar[0])
769 skipFile = TRUE;
770 else if (toupper(answer[0]) != yesChar[0])
771 answered = FALSE;
775 /* See if it has to exist! */
776 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
777 skipFile = TRUE;
780 /* Output a status message */
781 if (!skipFile) {
782 if (flags & OPT_QUIET) {
783 /* Skip message */
784 } else if (flags & OPT_FULL) {
785 const WCHAR infostr[] = {'%', 's', ' ', '-', '>', ' ',
786 '%', 's', '\n', 0};
788 XCOPY_wprintf(infostr, copyFrom, copyTo);
789 } else {
790 const WCHAR infostr[] = {'%', 's', '\n', 0};
791 XCOPY_wprintf(infostr, copyFrom);
794 /* If allowing overwriting of read only files, remove any
795 write protection */
796 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
797 (flags & OPT_REPLACEREAD)) {
798 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
801 copiedFile = TRUE;
802 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
803 /* Skip copy */
804 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
806 DWORD error = GetLastError();
807 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
808 copyFrom, copyTo, error);
809 XCOPY_FailMessage(error);
811 if (flags & OPT_IGNOREERRORS) {
812 skipFile = TRUE;
813 } else {
814 ret = RC_WRITEERROR;
815 goto cleanup;
819 /* If /M supplied, remove the archive bit after successful copy */
820 if (!skipFile) {
821 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
822 (flags & OPT_REMOVEARCH)) {
823 SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
825 filesCopied++;
830 /* Find next file */
831 findres = FindNextFileW(h, finddata);
833 FindClose(h);
835 /* Search 2 - do subdirs */
836 if (flags & OPT_RECURSIVE) {
837 lstrcpyW(inputpath, srcstem);
838 lstrcatW(inputpath, wchr_star);
839 findres = TRUE;
840 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
842 h = FindFirstFileW(inputpath, finddata);
843 while (h != INVALID_HANDLE_VALUE && findres) {
845 /* Only looking for dirs */
846 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
847 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
848 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
850 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
852 /* Make up recursive information */
853 lstrcpyW(inputpath, srcstem);
854 lstrcatW(inputpath, finddata->cFileName);
855 lstrcatW(inputpath, wchr_slash);
857 lstrcpyW(outputpath, deststem);
858 if (*destspec == 0x00) {
859 lstrcatW(outputpath, finddata->cFileName);
861 /* If /E is supplied, create the directory now */
862 if ((flags & OPT_EMPTYDIR) &&
863 !(flags & OPT_SIMULATE))
864 XCOPY_CreateDirectory(outputpath);
866 lstrcatW(outputpath, wchr_slash);
869 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
872 /* Find next one */
873 findres = FindNextFileW(h, finddata);
877 cleanup:
879 /* free up memory */
880 HeapFree(GetProcessHeap(), 0, finddata);
881 HeapFree(GetProcessHeap(), 0, inputpath);
882 HeapFree(GetProcessHeap(), 0, outputpath);
884 return ret;
887 /* =========================================================================
888 * Routine copied from cmd.exe md command -
889 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
890 * dir2 if they do not already exist.
891 * ========================================================================= */
892 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
894 int len;
895 WCHAR *new_path;
896 BOOL ret = TRUE;
898 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
899 lstrcpyW(new_path,path);
901 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
902 new_path[len - 1] = 0;
904 while (!CreateDirectoryW(new_path,NULL))
906 WCHAR *slash;
907 DWORD last_error = GetLastError();
908 if (last_error == ERROR_ALREADY_EXISTS)
909 break;
911 if (last_error != ERROR_PATH_NOT_FOUND)
913 ret = FALSE;
914 break;
917 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
919 ret = FALSE;
920 break;
923 len = slash - new_path;
924 new_path[len] = 0;
925 if (!XCOPY_CreateDirectory(new_path))
927 ret = FALSE;
928 break;
930 new_path[len] = '\\';
932 HeapFree(GetProcessHeap(),0,new_path);
933 return ret;
936 /* =========================================================================
937 * Process the /EXCLUDE: file list, building up a list of substrings to
938 * avoid copying
939 * Returns TRUE on any failure
940 * ========================================================================= */
941 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
943 WCHAR *filenameStart = parms;
945 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
946 excludeList = NULL;
948 while (*parms && *parms != ' ' && *parms != '/') {
950 /* If found '+' then process the file found so far */
951 if (*parms == '+') {
952 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
953 return TRUE;
955 filenameStart = parms+1;
957 parms++;
960 if (filenameStart != parms) {
961 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
962 return TRUE;
966 return FALSE;
969 /* =========================================================================
970 * Process a single file from the /EXCLUDE: file list, building up a list
971 * of substrings to avoid copying
972 * Returns TRUE on any failure
973 * ========================================================================= */
974 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
976 WCHAR endChar = *endOfName;
977 WCHAR buffer[MAXSTRING];
978 FILE *inFile = NULL;
979 const WCHAR readTextMode[] = {'r', 't', 0};
981 /* Null terminate the filename (temporarily updates the filename hence
982 parms not const) */
983 *endOfName = 0x00;
985 /* Open the file */
986 inFile = _wfopen(filename, readTextMode);
987 if (inFile == NULL) {
988 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
989 *endOfName = endChar;
990 return TRUE;
993 /* Process line by line */
994 while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) {
995 EXCLUDELIST *thisEntry;
996 int length = lstrlenW(buffer);
998 /* Strip CRLF */
999 buffer[length-1] = 0x00;
1001 /* If more than CRLF */
1002 if (length > 1) {
1003 thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
1004 thisEntry->next = excludeList;
1005 excludeList = thisEntry;
1006 thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
1007 (length * sizeof(WCHAR))+1);
1008 lstrcpyW(thisEntry->name, buffer);
1009 CharUpperBuffW(thisEntry->name, length);
1010 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
1014 /* See if EOF or error occurred */
1015 if (!feof(inFile)) {
1016 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
1017 *endOfName = endChar;
1018 return TRUE;
1021 /* Revert the input string to original form, and cleanup + return */
1022 *endOfName = endChar;
1023 fclose(inFile);
1024 return FALSE;
1027 /* =========================================================================
1028 * Load a string from the resource file, handling any error
1029 * Returns string retrieved from resource file
1030 * ========================================================================= */
1031 static WCHAR *XCOPY_LoadMessage(UINT id) {
1032 static WCHAR msg[MAXSTRING];
1033 const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
1035 if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) {
1036 WINE_FIXME("LoadString failed with %d\n", GetLastError());
1037 lstrcpyW(msg, failedMsg);
1039 return msg;
1042 /* =========================================================================
1043 * Load a string for a system error and writes it to the screen
1044 * Returns string retrieved from resource file
1045 * ========================================================================= */
1046 static void XCOPY_FailMessage(DWORD err) {
1047 LPWSTR lpMsgBuf;
1048 int status;
1050 status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
1051 FORMAT_MESSAGE_FROM_SYSTEM,
1052 NULL, err, 0,
1053 (LPWSTR) &lpMsgBuf, 0, NULL);
1054 if (!status) {
1055 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
1056 err, GetLastError());
1057 } else {
1058 const WCHAR infostr[] = {'%', 's', '\n', 0};
1059 XCOPY_wprintf(infostr, lpMsgBuf);
1060 LocalFree ((HLOCAL)lpMsgBuf);
1064 /* =========================================================================
1065 * Output a formatted unicode string. Ideally this will go to the console
1066 * and hence required WriteConsoleW to output it, however if file i/o is
1067 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
1068 * ========================================================================= */
1069 int XCOPY_wprintf(const WCHAR *format, ...) {
1071 static WCHAR *output_bufW = NULL;
1072 static char *output_bufA = NULL;
1073 static BOOL toConsole = TRUE;
1074 static BOOL traceOutput = FALSE;
1075 #define MAX_WRITECONSOLE_SIZE 65535
1077 va_list parms;
1078 DWORD nOut;
1079 int len;
1080 DWORD res = 0;
1083 * Allocate buffer to use when writing to console
1084 * Note: Not freed - memory will be allocated once and released when
1085 * xcopy ends
1088 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
1089 MAX_WRITECONSOLE_SIZE);
1090 if (!output_bufW) {
1091 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1092 return 0;
1095 va_start(parms, format);
1096 len = vsnprintfW(output_bufW, MAX_WRITECONSOLE_SIZE/sizeof(WCHAR), format, parms);
1097 va_end(parms);
1098 if (len < 0) {
1099 WINE_FIXME("String too long.\n");
1100 return 0;
1103 /* Try to write as unicode all the time we think its a console */
1104 if (toConsole) {
1105 res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
1106 output_bufW, len, &nOut, NULL);
1109 /* If writing to console has failed (ever) we assume its file
1110 i/o so convert to OEM codepage and output */
1111 if (!res) {
1112 BOOL usedDefaultChar = FALSE;
1113 DWORD convertedChars;
1115 toConsole = FALSE;
1118 * Allocate buffer to use when writing to file. Not freed, as above
1120 if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
1121 MAX_WRITECONSOLE_SIZE);
1122 if (!output_bufA) {
1123 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1124 return 0;
1127 /* Convert to OEM, then output */
1128 convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
1129 len, output_bufA, MAX_WRITECONSOLE_SIZE,
1130 "?", &usedDefaultChar);
1131 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
1132 &nOut, FALSE);
1135 /* Trace whether screen or console */
1136 if (!traceOutput) {
1137 WINE_TRACE("Writing to console? (%d)\n", toConsole);
1138 traceOutput = TRUE;
1140 return nOut;