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