1 /* 2 * XCOPY - Wine-compatible xcopy program 3 * 4 * Copyright (C) 2007 J. Edmeades 5 * 6 * This library is free software; you can redistribute it and/or 7 * modify it under the terms of the GNU Lesser General Public 8 * License as published by the Free Software Foundation; either 9 * version 2.1 of the License, or (at your option) any later version. 10 * 11 * This library is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 * Lesser General Public License for more details. 15 * 16 * You should have received a copy of the GNU Lesser General Public 17 * License along with this library; if not, write to the Free Software 18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA 19 */ 20 21 /* 22 * FIXME: 23 * This should now support all options listed in the xcopy help from 24 * windows XP except: 25 * /Z - Copy from network drives in restartable mode 26 * /X - Copy file audit settings (sets /O) 27 * /O - Copy file ownership + ACL info 28 * /G - Copy encrypted files to unencrypted destination 29 * /V - Verifies files 30 */ 31 32 /* 33 * Notes: 34 * Apparently, valid return codes are: 35 * 0 - OK 36 * 1 - No files found to copy 37 * 2 - CTRL+C during copy 38 * 4 - Initialization error, or invalid source specification 39 * 5 - Disk write error 40 */ 41 42 43 #include <stdio.h> 44 #include <windows.h> 45 #include <wine/debug.h> 46 #include <wine/unicode.h> 47 #include "xcopy.h" 48 49 WINE_DEFAULT_DEBUG_CHANNEL(xcopy); 50 51 /* 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, ...); 67 68 /* Typedefs */ 69 typedef struct _EXCLUDELIST 70 { 71 struct _EXCLUDELIST *next; 72 WCHAR *name; 73 } EXCLUDELIST; 74 75 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}; 84 85 /* Constants (Mostly for widechars) */ 86 87 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]; 92 93 94 /* ========================================================================= 95 main - Main entrypoint for the xcopy command 96 97 Processes the args, and drives the actual copying 98 ========================================================================= */ 99 int wmain (int argc, WCHAR *argvW[]) 100 { 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}; 113 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; 119 } 120 } 121 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; 128 129 /* 130 * Parse the command line 131 */ 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; 138 } 139 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)); 144 145 /* Extract required information from source specification */ 146 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags); 147 if (rc != RC_OK) return rc; 148 149 /* Extract required information from destination specification */ 150 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem, 151 destinationspec, sourcespec, flags); 152 if (rc != RC_OK) return rc; 153 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)); 160 161 /* Pause if necessary */ 162 if (flags & OPT_PAUSE) { 163 DWORD count; 164 char pausestr[10]; 165 166 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE)); 167 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr), 168 &count, NULL); 169 } 170 171 /* Now do the hard work... */ 172 rc = XCOPY_DoCopy(sourcestem, sourcespec, 173 destinationstem, destinationspec, 174 flags); 175 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); 182 } 183 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); 189 } 190 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES; 191 return rc; 192 193 } 194 195 /* ========================================================================= 196 XCOPY_ParseCommandLine - Parses the command line 197 ========================================================================= */ 198 static BOOL is_whitespace(WCHAR c) 199 { 200 return c == ' ' || c == '\t'; 201 } 202 203 static WCHAR *skip_whitespace(WCHAR *p) 204 { 205 for (; *p && is_whitespace(*p); p++); 206 return p; 207 } 208 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) 214 { 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++; 223 } 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; 229 } 230 *end = (WCHAR*)ptr; 231 return RC_OK; 232 } 233 234 /* Remove all double quotes from a word */ 235 static void strip_quotes(WCHAR *word, WCHAR **end) 236 { 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++; 244 } 245 *wp = '\0'; 246 *end = wp; 247 } 248 249 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource, 250 WCHAR *supplieddestination, DWORD *pflags) 251 { 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; 256 257 cmdline = _wcsdup(GetCommandLineW()); 258 if (cmdline == NULL) 259 return rc; 260 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); 265 266 while (*word) 267 { 268 WCHAR first; 269 if ((rc = find_end_of_word(word, &end)) != RC_OK) 270 goto out; 271 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)); 277 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; 287 } 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; 311 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; 323 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)); 330 331 /* Parse the arg : Month */ 332 st.wMonth = _wtol(pos); 333 while (*pos && isdigit(*pos)) pos++; 334 if (*pos++ != '-') isError = TRUE; 335 336 /* Parse the arg : Day */ 337 if (!isError) { 338 st.wDay = _wtol(pos); 339 while (*pos && isdigit(*pos)) pos++; 340 if (*pos++ != '-') isError = TRUE; 341 } 342 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; 348 } 349 350 if (!isError && SystemTimeToFileTime(&st, &dateRange)) { 351 SYSTEMTIME st; 352 WCHAR datestring[32], timestring[32]; 353 354 flags |= OPT_DATERANGE; 355 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)); 362 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; 368 } 369 } else { 370 flags |= OPT_DATENEWER; 371 } 372 break; 373 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; 383 } 384 } 385 word = next; 386 } 387 388 /* Default the destination if not supplied */ 389 if (supplieddestination[0] == 0x00) 390 lstrcpyW(supplieddestination, wchr_dot); 391 392 *pflags = flags; 393 rc = RC_OK; 394 395 out: 396 free(cmdline); 397 return rc; 398 } 399 400 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) 407 { 408 WCHAR actualsource[MAX_PATH]; 409 WCHAR *starPos; 410 WCHAR *questPos; 411 DWORD attribs; 412 413 /* 414 * Validate the source, expanding to full path ensuring it exists 415 */ 416 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) { 417 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError()); 418 return RC_INITERROR; 419 } 420 421 /* If full names required, convert to using the full path */ 422 if (flags & OPT_FULL) { 423 lstrcpyW(suppliedsource, actualsource); 424 } 425 426 /* 427 * Work out the stem of the source 428 */ 429 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 */ 438 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); 445 } 446 447 if (attribs == INVALID_FILE_ATTRIBUTES) { 448 XCOPY_FailMessage(GetLastError()); 449 return RC_INITERROR; 450 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; 456 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); 462 } 463 lstrcpyW(spec, wchr_star); 464 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; 472 473 WINE_TRACE("Filename supplied\n"); 474 lastDir = wcsrchr(suppliedsource, '\\'); 475 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); 491 } 492 } 493 494 return RC_OK; 495 } 496 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) 503 { 504 WCHAR actualdestination[MAX_PATH]; 505 DWORD attribs; 506 BOOL isDir = FALSE; 507 508 /* 509 * Validate the source, expanding to full path ensuring it exists 510 */ 511 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) { 512 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError()); 513 return RC_INITERROR; 514 } 515 516 /* Destination is either a directory or a file */ 517 attribs = GetFileAttributesW(actualdestination); 518 519 if (attribs == INVALID_FILE_ATTRIBUTES) { 520 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] == '\\')) { 526 527 isDir = TRUE; 528 529 } else { 530 DWORD count; 531 char answer[10] = ""; 532 WCHAR fileChar[2]; 533 WCHAR dirChar[2]; 534 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)); 538 539 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) { 540 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination); 541 542 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL); 543 WINE_TRACE("User answer %c\n", answer[0]); 544 545 answer[0] = toupper(answer[0]); 546 } 547 548 if (answer[0] == dirChar[0]) { 549 isDir = TRUE; 550 } else { 551 isDir = FALSE; 552 } 553 } 554 } else { 555 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY); 556 } 557 558 if (isDir) { 559 lstrcpyW(stem, actualdestination); 560 *spec = 0x00; 561 562 /* Ensure ends with a '\' */ 563 if (stem[lstrlenW(stem)-1] != '\\') { 564 lstrcatW(stem, wchr_slash); 565 } 566 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); 577 } 578 return RC_OK; 579 } 580 581 /* ========================================================================= 582 XCOPY_DoCopy - Recursive function to copy files based on input parms 583 of a stem and a spec 584 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 589 590 ========================================================================= */ 591 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec, 592 WCHAR *deststem, WCHAR *destspec, 593 DWORD flags) 594 { 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; 603 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)); 608 609 /* Build the search info into a single parm */ 610 lstrcpyW(inputpath, srcstem); 611 lstrcatW(inputpath, srcspec); 612 613 /* Search 1 - Look for matching files */ 614 h = FindFirstFileW(inputpath, finddata); 615 while (h != INVALID_HANDLE_VALUE && findres) { 616 617 skipFile = FALSE; 618 619 /* Ignore . and .. */ 620 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 || 621 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 || 622 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { 623 624 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName)); 625 } else { 626 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); 633 } 634 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); 641 } 642 } else { 643 lstrcatW(copyTo, destspec); 644 } 645 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); 650 651 /* See if allowed to copy it */ 652 srcAttribs = GetFileAttributesW(copyFrom); 653 WINE_TRACE("Source attribs: %d\n", srcAttribs); 654 655 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) || 656 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) { 657 658 if (!(flags & OPT_COPYHIDSYS)) { 659 skipFile = TRUE; 660 } 661 } 662 663 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) && 664 (flags & OPT_ARCHIVEONLY)) { 665 skipFile = TRUE; 666 } 667 668 /* See if file exists */ 669 destAttribs = GetFileAttributesW(copyTo); 670 WINE_TRACE("Dest attribs: %d\n", srcAttribs); 671 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; 677 } 678 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); 688 689 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) { 690 WINE_TRACE("Skipping file as dest newer or same date\n"); 691 skipFile = TRUE; 692 } 693 CloseHandle(h); 694 } 695 } 696 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]; 703 704 /* Uppercase source filename */ 705 lstrcpyW(copyFromUpper, copyFrom); 706 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper)); 707 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; 717 } 718 } 719 } 720 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]; 728 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)); 732 733 while (!answered) { 734 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom); 735 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), 736 &count, NULL); 737 738 answered = TRUE; 739 if (toupper(answer[0]) == noChar[0]) 740 skipFile = TRUE; 741 else if (toupper(answer[0]) != yesChar[0]) 742 answered = FALSE; 743 } 744 } 745 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]; 754 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)); 759 760 while (!answered) { 761 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo); 762 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), 763 &count, NULL); 764 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; 772 } 773 } 774 775 /* See if it has to exist! */ 776 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) { 777 skipFile = TRUE; 778 } 779 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}; 787 788 XCOPY_wprintf(infostr, copyFrom, copyTo); 789 } else { 790 const WCHAR infostr[] = {'%', 's', '\n', 0}; 791 XCOPY_wprintf(infostr, copyFrom); 792 } 793 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); 799 } 800 801 copiedFile = TRUE; 802 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) { 803 /* Skip copy */ 804 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) { 805 806 DWORD error = GetLastError(); 807 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL), 808 copyFrom, copyTo, error); 809 XCOPY_FailMessage(error); 810 811 if (flags & OPT_IGNOREERRORS) { 812 skipFile = TRUE; 813 } else { 814 ret = RC_WRITEERROR; 815 goto cleanup; 816 } 817 } 818 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)); 824 } 825 filesCopied++; 826 } 827 } 828 } 829 830 /* Find next file */ 831 findres = FindNextFileW(h, finddata); 832 } 833 FindClose(h); 834 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)); 841 842 h = FindFirstFileW(inputpath, finddata); 843 while (h != INVALID_HANDLE_VALUE && findres) { 844 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)) { 849 850 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName)); 851 852 /* Make up recursive information */ 853 lstrcpyW(inputpath, srcstem); 854 lstrcatW(inputpath, finddata->cFileName); 855 lstrcatW(inputpath, wchr_slash); 856 857 lstrcpyW(outputpath, deststem); 858 if (*destspec == 0x00) { 859 lstrcatW(outputpath, finddata->cFileName); 860 861 /* If /E is supplied, create the directory now */ 862 if ((flags & OPT_EMPTYDIR) && 863 !(flags & OPT_SIMULATE)) 864 XCOPY_CreateDirectory(outputpath); 865 866 lstrcatW(outputpath, wchr_slash); 867 } 868 869 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags); 870 } 871 872 /* Find next one */ 873 findres = FindNextFileW(h, finddata); 874 } 875 } 876 877 cleanup: 878 879 /* free up memory */ 880 HeapFree(GetProcessHeap(), 0, finddata); 881 HeapFree(GetProcessHeap(), 0, inputpath); 882 HeapFree(GetProcessHeap(), 0, outputpath); 883 884 return ret; 885 } 886 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) 893 { 894 int len; 895 WCHAR *new_path; 896 BOOL ret = TRUE; 897 898 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1)); 899 lstrcpyW(new_path,path); 900 901 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\') 902 new_path[len - 1] = 0; 903 904 while (!CreateDirectoryW(new_path,NULL)) 905 { 906 WCHAR *slash; 907 DWORD last_error = GetLastError(); 908 if (last_error == ERROR_ALREADY_EXISTS) 909 break; 910 911 if (last_error != ERROR_PATH_NOT_FOUND) 912 { 913 ret = FALSE; 914 break; 915 } 916 917 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/'))) 918 { 919 ret = FALSE; 920 break; 921 } 922 923 len = slash - new_path; 924 new_path[len] = 0; 925 if (!XCOPY_CreateDirectory(new_path)) 926 { 927 ret = FALSE; 928 break; 929 } 930 new_path[len] = '\\'; 931 } 932 HeapFree(GetProcessHeap(),0,new_path); 933 return ret; 934 } 935 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) { 942 943 WCHAR *filenameStart = parms; 944 945 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms)); 946 excludeList = NULL; 947 948 while (*parms && *parms != ' ' && *parms != '/') { 949 950 /* If found '+' then process the file found so far */ 951 if (*parms == '+') { 952 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) { 953 return TRUE; 954 } 955 filenameStart = parms+1; 956 } 957 parms++; 958 } 959 960 if (filenameStart != parms) { 961 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) { 962 return TRUE; 963 } 964 } 965 966 return FALSE; 967 } 968 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) { 975 976 WCHAR endChar = *endOfName; 977 WCHAR buffer[MAXSTRING]; 978 FILE *inFile = NULL; 979 const WCHAR readTextMode[] = {'r', 't', 0}; 980 981 /* Null terminate the filename (temporarily updates the filename hence 982 parms not const) */ 983 *endOfName = 0x00; 984 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; 991 } 992 993 /* Process line by line */ 994 while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) { 995 EXCLUDELIST *thisEntry; 996 int length = lstrlenW(buffer); 997 998 /* Strip CRLF */ 999 buffer[length-1] = 0x00; 1000 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)); 1011 } 1012 } 1013 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; 1019 } 1020 1021 /* Revert the input string to original form, and cleanup + return */ 1022 *endOfName = endChar; 1023 fclose(inFile); 1024 return FALSE; 1025 } 1026 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}; 1034 1035 if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) { 1036 WINE_FIXME("LoadString failed with %d\n", GetLastError()); 1037 lstrcpyW(msg, failedMsg); 1038 } 1039 return msg; 1040 } 1041 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; 1049 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); 1061 } 1062 } 1063 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, ...) { 1070 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 1076 1077 va_list parms; 1078 DWORD nOut; 1079 int len; 1080 DWORD res = 0; 1081 1082 /* 1083 * Allocate buffer to use when writing to console 1084 * Note: Not freed - memory will be allocated once and released when 1085 * xcopy ends 1086 */ 1087 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; 1093 } 1094 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; 1101 } 1102 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); 1107 } 1108 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; 1114 1115 toConsole = FALSE; 1116 1117 /* 1118 * Allocate buffer to use when writing to file. Not freed, as above 1119 */ 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; 1125 } 1126 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); 1133 } 1134 1135 /* Trace whether screen or console */ 1136 if (!traceOutput) { 1137 WINE_TRACE("Writing to console? (%d)\n", toConsole); 1138 traceOutput = TRUE; 1139 } 1140 return nOut; 1141 } 1142