xcopy.c 38.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 * XCOPY - Wine-compatible xcopy program
 *
 * Copyright (C) 2007 J. Edmeades
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

21 22 23 24 25 26 27 28 29 30 31
/*
 * FIXME:
 * This should now support all options listed in the xcopy help from
 * windows XP except:
 *  /Z - Copy from network drives in restartable mode
 *  /X - Copy file audit settings (sets /O)
 *  /O - Copy file ownership + ACL info
 *  /G - Copy encrypted files to unencrypted destination
 *  /V - Verifies files
 */

32 33 34 35 36 37 38 39 40 41 42 43 44 45
/*
 * Notes:
 * Apparently, valid return codes are:
 *   0 - OK
 *   1 - No files found to copy
 *   2 - CTRL+C during copy
 *   4 - Initialization error, or invalid source specification
 *   5 - Disk write error
 */


#include <stdio.h>
#include <windows.h>
#include <wine/debug.h>
46
#include <wine/unicode.h>
47
#include "xcopy.h"
48

49 50 51
WINE_DEFAULT_DEBUG_CHANNEL(xcopy);

/* Prototypes */
52 53
static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
                                   WCHAR *spec, DWORD flags);
54 55 56 57 58 59
static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
                                 WCHAR *spec, WCHAR *srcspec, DWORD flags);
static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
                        WCHAR *deststem, WCHAR *destspec,
                        DWORD flags);
static BOOL XCOPY_CreateDirectory(const WCHAR* path);
60 61
static BOOL XCOPY_ProcessExcludeList(WCHAR* parms);
static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName);
62 63
static WCHAR *XCOPY_LoadMessage(UINT id);
static void XCOPY_FailMessage(DWORD err);
64
static int XCOPY_wprintf(const WCHAR *format, ...);
65 66 67 68 69 70 71 72

/* Typedefs */
typedef struct _EXCLUDELIST
{
  struct _EXCLUDELIST *next;
  WCHAR               *name;
} EXCLUDELIST;

73 74 75

/* Global variables */
static ULONG filesCopied           = 0;              /* Number of files copied  */
76
static EXCLUDELIST *excludeList    = NULL;           /* Excluded strings list   */
77
static FILETIME dateRange;                           /* Date range to copy after*/
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
static const WCHAR wchr_slash[]   = {'\\', 0};
static const WCHAR wchr_star[]    = {'*', 0};
static const WCHAR wchr_dot[]     = {'.', 0};
static const WCHAR wchr_dotdot[]  = {'.', '.', 0};

/* Constants (Mostly for widechars) */


/* To minimize stack usage during recursion, some temporary variables
   made global                                                        */
static WCHAR copyFrom[MAX_PATH];
static WCHAR copyTo[MAX_PATH];


/* =========================================================================
   main - Main entrypoint for the xcopy command

     Processes the args, and drives the actual copying
   ========================================================================= */
97
int wmain (int argc, WCHAR *argvW[])
98 99 100 101 102 103 104 105
{
    int     rc = 0;
    WCHAR   suppliedsource[MAX_PATH] = {0};   /* As supplied on the cmd line */
    WCHAR   supplieddestination[MAX_PATH] = {0};
    WCHAR   sourcestem[MAX_PATH] = {0};       /* Stem of source          */
    WCHAR   sourcespec[MAX_PATH] = {0};       /* Filespec of source      */
    WCHAR   destinationstem[MAX_PATH] = {0};  /* Stem of destination     */
    WCHAR   destinationspec[MAX_PATH] = {0};  /* Filespec of destination */
106
    WCHAR   copyCmd[MAXSTRING];               /* COPYCMD env var         */
107
    DWORD   flags = 0;                        /* Option flags            */
108 109 110
    const WCHAR PROMPTSTR1[]  = {'/', 'Y', 0};
    const WCHAR PROMPTSTR2[]  = {'/', 'y', 0};
    const WCHAR COPYCMD[]  = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
111
    const WCHAR EXCLUDE[]  = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
112

113 114 115 116 117 118
    /*
     * Parse the command line
     */

    /* Confirm at least one parameter */
    if (argc < 2) {
119
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
120 121 122
        return RC_INITERROR;
    }

123
    /* Preinitialize flags based on COPYCMD */
124
    if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
125 126 127 128 129 130
        if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
            wcsstr(copyCmd, PROMPTSTR2) != NULL) {
            flags |= OPT_NOPROMPT;
        }
    }

131 132 133 134 135 136 137
    /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
       wine, but on windows these can be normal files. At least one installer
       uses files such as .packlist and (validly) expects them to be copied.
       Under wine, if we do not copy hidden files by default then they get
       lose                                                                   */
    flags |= OPT_COPYHIDSYS;

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    /* Skip first arg, which is the program name */
    argvW++;

    while (argc > 1)
    {
        argc--;
        WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(*argvW));

        /* First non-switch parameter is source, second is destination */
        if (*argvW[0] != '/') {
            if (suppliedsource[0] == 0x00) {
                lstrcpyW(suppliedsource, *argvW);
            } else if (supplieddestination[0] == 0x00) {
                lstrcpyW(supplieddestination, *argvW);
            } else {
153
                XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
154 155 156
                return RC_INITERROR;
            }
        } else {
157 158 159 160
            /* Process all the switch options
                 Note: Windows docs say /P prompts when dest is created
                       but tests show it is done for each src file
                       regardless of the destination                   */
161 162 163 164 165 166
            switch (toupper(argvW[0][1])) {
            case 'I': flags |= OPT_ASSUMEDIR;     break;
            case 'S': flags |= OPT_RECURSIVE;     break;
            case 'Q': flags |= OPT_QUIET;         break;
            case 'F': flags |= OPT_FULL;          break;
            case 'L': flags |= OPT_SIMULATE;      break;
167
            case 'W': flags |= OPT_PAUSE;         break;
168
            case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
169
            case 'Y': flags |= OPT_NOPROMPT;      break;
170
            case 'N': flags |= OPT_SHORTNAME;     break;
171
            case 'U': flags |= OPT_MUSTEXIST;     break;
172
            case 'R': flags |= OPT_REPLACEREAD;   break;
173
            case 'H': flags |= OPT_COPYHIDSYS;    break;
174
            case 'C': flags |= OPT_IGNOREERRORS;  break;
175
            case 'P': flags |= OPT_SRCPROMPT;     break;
176 177 178
            case 'A': flags |= OPT_ARCHIVEONLY;   break;
            case 'M': flags |= OPT_ARCHIVEONLY |
                               OPT_REMOVEARCH;    break;
179 180

            /* E can be /E or /EXCLUDE */
181
            case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
182 183 184 185
                                         NORM_IGNORECASE | SORT_STRINGSORT,
                                         &argvW[0][1], 8,
                                         EXCLUDE, -1) == 2) {
                        if (XCOPY_ProcessExcludeList(&argvW[0][9])) {
186
                          XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
187 188
                          return RC_INITERROR;
                        } else flags |= OPT_EXCLUDELIST;
189
                      } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
190 191
                      break;

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
            /* D can be /D or /D: */
            case 'D': if ((argvW[0][2])==':' && isdigit(argvW[0][3])) {
                          SYSTEMTIME st;
                          WCHAR     *pos = &argvW[0][3];
                          BOOL       isError = FALSE;
                          memset(&st, 0x00, sizeof(st));

                          /* Parse the arg : Month */
                          st.wMonth = _wtol(pos);
                          while (*pos && isdigit(*pos)) pos++;
                          if (*pos++ != '-') isError = TRUE;

                          /* Parse the arg : Day */
                          if (!isError) {
                              st.wDay = _wtol(pos);
                              while (*pos && isdigit(*pos)) pos++;
                              if (*pos++ != '-') isError = TRUE;
                          }

                          /* Parse the arg : Day */
                          if (!isError) {
                              st.wYear = _wtol(pos);
                              if (st.wYear < 100) st.wYear+=2000;
                          }

                          if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
                              SYSTEMTIME st;
                              WCHAR datestring[32], timestring[32];

                              flags |= OPT_DATERANGE;

                              /* Debug info: */
                              FileTimeToSystemTime (&dateRange, &st);
225 226 227 228
                              GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
                                             sizeof(datestring)/sizeof(WCHAR));
                              GetTimeFormatW(0, TIME_NOSECONDS, &st,
                                             NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
229 230 231 232

                              WINE_TRACE("Date being used is: %s %s\n",
                                         wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
                          } else {
233
                              XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
234 235 236 237 238 239 240
                              return RC_INITERROR;
                          }
                      } else {
                          flags |= OPT_DATENEWER;
                      }
                      break;

241 242
            case '-': if (toupper(argvW[0][2])=='Y')
                          flags &= ~OPT_NOPROMPT; break;
243
            case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
Jason Edmeades's avatar
Jason Edmeades committed
244
                      return RC_OK;
245
            default:
246
                WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW));
247
                XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), *argvW);
248
                return RC_INITERROR;
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
            }
        }
        argvW++;
    }

    /* Default the destination if not supplied */
    if (supplieddestination[0] == 0x00)
        lstrcpyW(supplieddestination, wchr_dot);

    /* Trace out the supplied information */
    WINE_TRACE("Supplied parameters:\n");
    WINE_TRACE("Source      : '%s'\n", wine_dbgstr_w(suppliedsource));
    WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));

    /* Extract required information from source specification */
264
    rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
265 266 267 268 269 270 271 272 273 274 275 276

    /* Extract required information from destination specification */
    rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
                               destinationspec, sourcespec, flags);

    /* Trace out the resulting information */
    WINE_TRACE("Resolved parameters:\n");
    WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
    WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
    WINE_TRACE("Dest   Stem : '%s'\n", wine_dbgstr_w(destinationstem));
    WINE_TRACE("Dest   Spec : '%s'\n", wine_dbgstr_w(destinationspec));

277 278 279 280 281
    /* Pause if necessary */
    if (flags & OPT_PAUSE) {
        DWORD count;
        char pausestr[10];

282
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
283 284 285 286
        ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
                  &count, NULL);
    }

287 288 289 290 291
    /* Now do the hard work... */
    rc = XCOPY_DoCopy(sourcestem, sourcespec,
                destinationstem, destinationspec,
                flags);

292 293 294 295 296 297 298 299
    /* Clear up exclude list allocated memory */
    while (excludeList) {
        EXCLUDELIST *pos = excludeList;
        excludeList = excludeList -> next;
        HeapFree(GetProcessHeap(), 0, pos->name);
        HeapFree(GetProcessHeap(), 0, pos);
    }

300 301
    /* Finished - print trailer and exit */
    if (flags & OPT_SIMULATE) {
302
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
303
    } else if (!(flags & OPT_NOCOPY)) {
304
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
305 306 307 308 309 310 311 312 313 314 315
    }
    if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
    return rc;

}


/* =========================================================================
   XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
     converts it into a stem and a filespec
   ========================================================================= */
316 317
static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
                                   WCHAR *spec, DWORD flags)
318 319 320 321
{
    WCHAR             actualsource[MAX_PATH];
    WCHAR            *starPos;
    WCHAR            *questPos;
322
    DWORD             attribs;
323 324 325 326

    /*
     * Validate the source, expanding to full path ensuring it exists
     */
327
    if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
328 329 330 331
        WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
        return RC_INITERROR;
    }

332 333 334 335 336
    /* If full names required, convert to using the full path */
    if (flags & OPT_FULL) {
        lstrcpyW(suppliedsource, actualsource);
    }

337 338 339 340
    /*
     * Work out the stem of the source
     */

341 342 343 344 345 346 347 348 349 350 351
    /* If a directory is supplied, use that as-is (either fully or
          partially qualified)
       If a filename is supplied + a directory or drive path, use that
          as-is
       Otherwise
          If no directory or path specified, add eg. C:
          stem is Drive/Directory is bit up to last \ (or first :)
          spec is bit after that                                         */

    starPos = wcschr(suppliedsource, '*');
    questPos = wcschr(suppliedsource, '?');
352
    if (starPos || questPos) {
353
        attribs = 0x00;  /* Ensures skips invalid or directory check below */
354
    } else {
355
        attribs = GetFileAttributesW(actualsource);
356
    }
357

358 359 360
    if (attribs == INVALID_FILE_ATTRIBUTES) {
        XCOPY_FailMessage(GetLastError());
        return RC_INITERROR;
361

362 363 364 365 366
    /* Directory:
         stem should be exactly as supplied plus a '\', unless it was
          eg. C: in which case no slash required */
    } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
        WCHAR lastChar;
367

368 369 370 371
        WINE_TRACE("Directory supplied\n");
        lstrcpyW(stem, suppliedsource);
        lastChar = stem[lstrlenW(stem)-1];
        if (lastChar != '\\' && lastChar != ':') {
372
            lstrcatW(stem, wchr_slash);
373 374
        }
        lstrcpyW(spec, wchr_star);
375

376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
    /* File or wildcard search:
         stem should be:
           Up to and including last slash if directory path supplied
           If c:filename supplied, just the c:
           Otherwise stem should be the current drive letter + ':' */
    } else {
        WCHAR *lastDir;

        WINE_TRACE("Filename supplied\n");
        lastDir   = wcsrchr(suppliedsource, '\\');

        if (lastDir) {
            lstrcpyW(stem, suppliedsource);
            stem[(lastDir-suppliedsource) + 1] = 0x00;
            lstrcpyW(spec, (lastDir+1));
        } else if (suppliedsource[1] == ':') {
            lstrcpyW(stem, suppliedsource);
            stem[2] = 0x00;
            lstrcpyW(spec, suppliedsource+2);
395
        } else {
396
            WCHAR curdir[MAXSTRING];
397
            GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
398 399 400 401
            stem[0] = curdir[0];
            stem[1] = curdir[1];
            stem[2] = 0x00;
            lstrcpyW(spec, suppliedsource);
402 403
        }
    }
404

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    return RC_OK;
}

/* =========================================================================
   XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
     converts it into a stem
   ========================================================================= */
static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
                                 WCHAR *srcspec, DWORD flags)
{
    WCHAR  actualdestination[MAX_PATH];
    DWORD attribs;
    BOOL isDir = FALSE;

    /*
     * Validate the source, expanding to full path ensuring it exists
     */
422
    if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
423 424 425 426 427
        WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
        return RC_INITERROR;
    }

    /* Destination is either a directory or a file */
428
    attribs = GetFileAttributesW(actualdestination);
429 430 431 432 433 434 435 436 437 438 439 440

    if (attribs == INVALID_FILE_ATTRIBUTES) {

        /* If /I supplied and wildcard copy, assume directory */
        if (flags & OPT_ASSUMEDIR &&
            (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) {

            isDir = TRUE;

        } else {
            DWORD count;
            char  answer[10] = "";
441 442 443 444 445 446
            WCHAR fileChar[2];
            WCHAR dirChar[2];

            /* Read the F and D characters from the resource file */
            wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
            wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
447

448
            while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
449
                XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
450 451 452 453 454 455 456

                ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
                WINE_TRACE("User answer %c\n", answer[0]);

                answer[0] = toupper(answer[0]);
            }

457
            if (answer[0] == dirChar[0]) {
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
                isDir = TRUE;
            } else {
                isDir = FALSE;
            }
        }
    } else {
        isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
    }

    if (isDir) {
        lstrcpyW(stem, actualdestination);
        *spec = 0x00;

        /* Ensure ends with a '\' */
        if (stem[lstrlenW(stem)-1] != '\\') {
            lstrcatW(stem, wchr_slash);
        }

    } else {
        WCHAR drive[MAX_PATH];
        WCHAR dir[MAX_PATH];
        WCHAR fname[MAX_PATH];
        WCHAR ext[MAX_PATH];
        _wsplitpath(actualdestination, drive, dir, fname, ext);
        lstrcpyW(stem, drive);
        lstrcatW(stem, dir);
        lstrcpyW(spec, fname);
        lstrcatW(spec, ext);
    }
    return RC_OK;
}

/* =========================================================================
   XCOPY_DoCopy - Recursive function to copy files based on input parms
     of a stem and a spec

      This works by using FindFirstFile supplying the source stem and spec.
      If results are found, any non-directory ones are processed
      Then, if /S or /E is supplied, another search is made just for
      directories, and this function is called again for that directory

   ========================================================================= */
static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
                        WCHAR *deststem, WCHAR *destspec,
                        DWORD flags)
{
504
    WIN32_FIND_DATAW *finddata;
505 506 507 508
    HANDLE          h;
    BOOL            findres = TRUE;
    WCHAR           *inputpath, *outputpath;
    BOOL            copiedFile = FALSE;
509
    DWORD           destAttribs, srcAttribs;
510
    BOOL            skipFile;
511
    int             ret = 0;
512 513

    /* Allocate some working memory on heap to minimize footprint */
514
    finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
515 516 517 518 519 520 521 522
    inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
    outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));

    /* Build the search info into a single parm */
    lstrcpyW(inputpath, srcstem);
    lstrcatW(inputpath, srcspec);

    /* Search 1 - Look for matching files */
523
    h = FindFirstFileW(inputpath, finddata);
524 525
    while (h != INVALID_HANDLE_VALUE && findres) {

526 527
        skipFile = FALSE;

528 529 530 531 532 533 534 535 536 537
        /* Ignore . and .. */
        if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
            lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
            finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {

            WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
        } else {

            /* Get the filename information */
            lstrcpyW(copyFrom, srcstem);
538 539 540 541 542
            if (flags & OPT_SHORTNAME) {
              lstrcatW(copyFrom, finddata->cAlternateFileName);
            } else {
              lstrcatW(copyFrom, finddata->cFileName);
            }
543 544 545

            lstrcpyW(copyTo, deststem);
            if (*destspec == 0x00) {
546 547 548 549 550
                if (flags & OPT_SHORTNAME) {
                    lstrcatW(copyTo, finddata->cAlternateFileName);
                } else {
                    lstrcatW(copyTo, finddata->cFileName);
                }
551 552 553 554 555 556 557 558 559
            } else {
                lstrcatW(copyTo, destspec);
            }

            /* Do the copy */
            WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
                                                      wine_dbgstr_w(copyTo));
            if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);

560 561
            /* See if allowed to copy it */
            srcAttribs = GetFileAttributesW(copyFrom);
562 563
            WINE_TRACE("Source attribs: %d\n", srcAttribs);

564 565 566 567 568 569 570 571
            if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
                (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {

                if (!(flags & OPT_COPYHIDSYS)) {
                    skipFile = TRUE;
                }
            }

572 573 574 575 576
            if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
                (flags & OPT_ARCHIVEONLY)) {
                skipFile = TRUE;
            }

577 578
            /* See if file exists */
            destAttribs = GetFileAttributesW(copyTo);
579 580
            WINE_TRACE("Dest attribs: %d\n", srcAttribs);

581 582 583 584 585 586 587 588 589 590
            /* Check date ranges if a destination file already exists */
            if (!skipFile && (flags & OPT_DATERANGE) &&
                (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
                WINE_TRACE("Skipping file as modified date too old\n");
                skipFile = TRUE;
            }

            /* If just /D supplied, only overwrite if src newer than dest */
            if (!skipFile && (flags & OPT_DATENEWER) &&
               (destAttribs != INVALID_FILE_ATTRIBUTES)) {
591
                HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
592 593 594 595 596 597 598 599 600 601 602 603 604 605
                                      NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
                                      NULL);
                if (h != INVALID_HANDLE_VALUE) {
                    FILETIME writeTime;
                    GetFileTime(h, NULL, NULL, &writeTime);

                    if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
                        WINE_TRACE("Skipping file as dest newer or same date\n");
                        skipFile = TRUE;
                    }
                    CloseHandle(h);
                }
            }

606 607 608 609 610 611 612 613 614
            /* See if exclude list provided. Note since filenames are case
               insensitive, need to uppercase the filename before doing
               strstr                                                     */
            if (!skipFile && (flags & OPT_EXCLUDELIST)) {
                EXCLUDELIST *pos = excludeList;
                WCHAR copyFromUpper[MAX_PATH];

                /* Uppercase source filename */
                lstrcpyW(copyFromUpper, copyFrom);
615
                CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634

                /* Loop through testing each exclude line */
                while (pos) {
                    if (wcsstr(copyFromUpper, pos->name) != NULL) {
                        WINE_TRACE("Skipping file as matches exclude '%s'\n",
                                   wine_dbgstr_w(pos->name));
                        skipFile = TRUE;
                        pos = NULL;
                    } else {
                        pos = pos->next;
                    }
                }
            }

            /* Prompt each file if necessary */
            if (!skipFile && (flags & OPT_SRCPROMPT)) {
                DWORD count;
                char  answer[10];
                BOOL  answered = FALSE;
635 636 637 638 639 640
                WCHAR yesChar[2];
                WCHAR noChar[2];

                /* Read the Y and N characters from the resource file */
                wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
                wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
641 642

                while (!answered) {
643
                    XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
644 645 646 647
                    ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
                              &count, NULL);

                    answered = TRUE;
648
                    if (toupper(answer[0]) == noChar[0])
649
                        skipFile = TRUE;
650
                    else if (toupper(answer[0]) != yesChar[0])
651 652 653 654
                        answered = FALSE;
                }
            }

655 656
            if (!skipFile &&
                destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
657 658 659
                DWORD count;
                char  answer[10];
                BOOL  answered = FALSE;
660 661 662 663 664 665 666 667
                WCHAR yesChar[2];
                WCHAR allChar[2];
                WCHAR noChar[2];

                /* Read the A,Y and N characters from the resource file */
                wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
                wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
                wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
668 669

                while (!answered) {
670
                    XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
671 672 673 674
                    ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
                              &count, NULL);

                    answered = TRUE;
675
                    if (toupper(answer[0]) == allChar[0])
676
                        flags |= OPT_NOPROMPT;
677
                    else if (toupper(answer[0]) == noChar[0])
678
                        skipFile = TRUE;
679
                    else if (toupper(answer[0]) != yesChar[0])
680 681
                        answered = FALSE;
                }
682 683
            }

684 685 686 687 688
            /* See if it has to exist! */
            if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
                skipFile = TRUE;
            }

689 690 691 692 693
            /* Output a status message */
            if (!skipFile) {
                if (flags & OPT_QUIET) {
                    /* Skip message */
                } else if (flags & OPT_FULL) {
694 695 696 697
                    const WCHAR infostr[]   = {'%', 's', ' ', '-', '>', ' ',
                                               '%', 's', '\n', 0};

                    XCOPY_wprintf(infostr, copyFrom, copyTo);
698
                } else {
699 700
                    const WCHAR infostr[] = {'%', 's', '\n', 0};
                    XCOPY_wprintf(infostr, copyFrom);
701 702
                }

703 704 705 706
                /* If allowing overwriting of read only files, remove any
                   write protection                                       */
                if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
                    (flags & OPT_REPLACEREAD)) {
707
                    SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
708 709
                }

710 711 712
                copiedFile = TRUE;
                if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
                    /* Skip copy */
713
                } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
714 715

                    DWORD error = GetLastError();
716
                    XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
717 718 719
                           copyFrom, copyTo, error);
                    XCOPY_FailMessage(error);

720 721 722
                    if (flags & OPT_IGNOREERRORS) {
                        skipFile = TRUE;
                    } else {
723 724
                        ret = RC_WRITEERROR;
                        goto cleanup;
725
                    }
726
                }
727 728 729 730 731

                /* If /M supplied, remove the archive bit after successful copy */
                if (!skipFile) {
                    if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
                        (flags & OPT_REMOVEARCH)) {
732
                        SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
733 734 735
                    }
                    filesCopied++;
                }
736 737 738 739
            }
        }

        /* Find next file */
740
        findres = FindNextFileW(h, finddata);
741 742 743 744 745 746 747 748 749 750
    }
    FindClose(h);

    /* Search 2 - do subdirs */
    if (flags & OPT_RECURSIVE) {
        lstrcpyW(inputpath, srcstem);
        lstrcatW(inputpath, wchr_star);
        findres = TRUE;
        WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));

751
        h = FindFirstFileW(inputpath, finddata);
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
        while (h != INVALID_HANDLE_VALUE && findres) {

            /* Only looking for dirs */
            if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
                (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
                (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {

                WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));

                /* Make up recursive information */
                lstrcpyW(inputpath, srcstem);
                lstrcatW(inputpath, finddata->cFileName);
                lstrcatW(inputpath, wchr_slash);

                lstrcpyW(outputpath, deststem);
                if (*destspec == 0x00) {
                    lstrcatW(outputpath, finddata->cFileName);

                    /* If /E is supplied, create the directory now */
                    if ((flags & OPT_EMPTYDIR) &&
                        !(flags & OPT_SIMULATE))
                        XCOPY_CreateDirectory(outputpath);

                    lstrcatW(outputpath, wchr_slash);
                }

                XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
            }

            /* Find next one */
782
            findres = FindNextFileW(h, finddata);
783 784 785
        }
    }

786 787
cleanup:

788 789 790 791 792
    /* free up memory */
    HeapFree(GetProcessHeap(), 0, finddata);
    HeapFree(GetProcessHeap(), 0, inputpath);
    HeapFree(GetProcessHeap(), 0, outputpath);

793
    return ret;
794 795 796 797
}

/* =========================================================================
 * Routine copied from cmd.exe md command -
798
 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
799 800 801 802 803 804 805 806 807 808 809 810 811 812
 * dir2 if they do not already exist.
 * ========================================================================= */
static BOOL XCOPY_CreateDirectory(const WCHAR* path)
{
    int len;
    WCHAR *new_path;
    BOOL ret = TRUE;

    new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
    lstrcpyW(new_path,path);

    while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
        new_path[len - 1] = 0;

813
    while (!CreateDirectoryW(new_path,NULL))
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843
    {
        WCHAR *slash;
        DWORD last_error = GetLastError();
        if (last_error == ERROR_ALREADY_EXISTS)
            break;

        if (last_error != ERROR_PATH_NOT_FOUND)
        {
            ret = FALSE;
            break;
        }

        if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
        {
            ret = FALSE;
            break;
        }

        len = slash - new_path;
        new_path[len] = 0;
        if (!XCOPY_CreateDirectory(new_path))
        {
            ret = FALSE;
            break;
        }
        new_path[len] = '\\';
    }
    HeapFree(GetProcessHeap(),0,new_path);
    return ret;
}
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896

/* =========================================================================
 * Process the /EXCLUDE: file list, building up a list of substrings to
 * avoid copying
 * Returns TRUE on any failure
 * ========================================================================= */
static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {

    WCHAR *filenameStart = parms;

    WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
    excludeList = NULL;

    while (*parms && *parms != ' ' && *parms != '/') {

        /* If found '+' then process the file found so far */
        if (*parms == '+') {
            if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
                return TRUE;
            }
            filenameStart = parms+1;
        }
        parms++;
    }

    if (filenameStart != parms) {
        if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
            return TRUE;
        }
    }

    return FALSE;
}

/* =========================================================================
 * Process a single file from the /EXCLUDE: file list, building up a list
 * of substrings to avoid copying
 * Returns TRUE on any failure
 * ========================================================================= */
static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {

    WCHAR   endChar = *endOfName;
    WCHAR   buffer[MAXSTRING];
    FILE   *inFile  = NULL;
    const WCHAR readTextMode[]  = {'r', 't', 0};

    /* Null terminate the filename (temporarily updates the filename hence
         parms not const)                                                 */
    *endOfName = 0x00;

    /* Open the file */
    inFile = _wfopen(filename, readTextMode);
    if (inFile == NULL) {
897
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
898 899 900 901 902
        *endOfName = endChar;
        return TRUE;
    }

    /* Process line by line */
903
    while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) {
904 905 906 907 908 909
        EXCLUDELIST *thisEntry;
        int length = lstrlenW(buffer);

        /* Strip CRLF */
        buffer[length-1] = 0x00;

910 911 912 913 914 915 916 917
        /* If more than CRLF */
        if (length > 1) {
          thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
          thisEntry->next = excludeList;
          excludeList = thisEntry;
          thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
                                      (length * sizeof(WCHAR))+1);
          lstrcpyW(thisEntry->name, buffer);
918
          CharUpperBuffW(thisEntry->name, length);
919 920
          WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
        }
921 922 923 924
    }

    /* See if EOF or error occurred */
    if (!feof(inFile)) {
925
        XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
926 927 928 929 930 931 932 933 934
        *endOfName = endChar;
        return TRUE;
    }

    /* Revert the input string to original form, and cleanup + return */
    *endOfName = endChar;
    fclose(inFile);
    return FALSE;
}
935 936 937 938 939 940

/* =========================================================================
 * Load a string from the resource file, handling any error
 * Returns string retrieved from resource file
 * ========================================================================= */
static WCHAR *XCOPY_LoadMessage(UINT id) {
941
    static WCHAR msg[MAXSTRING];
942 943
    const WCHAR failedMsg[]  = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};

944
    if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) {
945 946 947 948 949 950 951 952 953 954 955 956 957 958
       WINE_FIXME("LoadString failed with %d\n", GetLastError());
       lstrcpyW(msg, failedMsg);
    }
    return msg;
}

/* =========================================================================
 * Load a string for a system error and writes it to the screen
 * Returns string retrieved from resource file
 * ========================================================================= */
static void XCOPY_FailMessage(DWORD err) {
    LPWSTR lpMsgBuf;
    int status;

959
    status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
960 961
                            FORMAT_MESSAGE_FROM_SYSTEM,
                            NULL, err, 0,
962
                            (LPWSTR) &lpMsgBuf, 0, NULL);
963 964 965 966
    if (!status) {
      WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
                 err, GetLastError());
    } else {
967 968
      const WCHAR infostr[] = {'%', 's', '\n', 0};
      XCOPY_wprintf(infostr, lpMsgBuf);
969 970 971
      LocalFree ((HLOCAL)lpMsgBuf);
    }
}
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986

/* =========================================================================
 * Output a formatted unicode string. Ideally this will go to the console
 *  and hence required WriteConsoleW to output it, however if file i/o is
 *  redirected, it needs to be WriteFile'd using OEM (not ANSI) format
 * ========================================================================= */
int XCOPY_wprintf(const WCHAR *format, ...) {

    static WCHAR *output_bufW = NULL;
    static char  *output_bufA = NULL;
    static BOOL  toConsole    = TRUE;
    static BOOL  traceOutput  = FALSE;
#define MAX_WRITECONSOLE_SIZE 65535

    va_list parms;
987 988
    DWORD   nOut;
    int len;
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
    DWORD   res = 0;

    /*
     * Allocate buffer to use when writing to console
     * Note: Not freed - memory will be allocated once and released when
     *         xcopy ends
     */

    if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
                                              MAX_WRITECONSOLE_SIZE);
    if (!output_bufW) {
      WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
      return 0;
    }

    va_start(parms, format);
1005
    len = vsnprintfW(output_bufW, MAX_WRITECONSOLE_SIZE/sizeof(WCHAR), format, parms);
1006
    va_end(parms);
1007 1008 1009 1010
    if (len < 0) {
      WINE_FIXME("String too long.\n");
      return 0;
    }
1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050

    /* Try to write as unicode all the time we think its a console */
    if (toConsole) {
      res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
                          output_bufW, len, &nOut, NULL);
    }

    /* If writing to console has failed (ever) we assume its file
       i/o so convert to OEM codepage and output                  */
    if (!res) {
      BOOL usedDefaultChar = FALSE;
      DWORD convertedChars;

      toConsole = FALSE;

      /*
       * Allocate buffer to use when writing to file. Not freed, as above
       */
      if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
                                                MAX_WRITECONSOLE_SIZE);
      if (!output_bufA) {
        WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
        return 0;
      }

      /* Convert to OEM, then output */
      convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
                          len, output_bufA, MAX_WRITECONSOLE_SIZE,
                          "?", &usedDefaultChar);
      WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
                &nOut, FALSE);
    }

    /* Trace whether screen or console */
    if (!traceOutput) {
      WINE_TRACE("Writing to console? (%d)\n", toConsole);
      traceOutput = TRUE;
    }
    return nOut;
}