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