| /* | |
| * 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 | |
| */ | |
| /* | |
| * 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 | |
| */ | |
| /* | |
| * 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> | |
| #include <wine/unicode.h> | |
| #include "xcopy.h" | |
| WINE_DEFAULT_DEBUG_CHANNEL(xcopy); | |
| /* Prototypes */ | |
| static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, | |
| WCHAR *spec, DWORD flags); | |
| 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); | |
| static BOOL XCOPY_ProcessExcludeList(WCHAR* parms); | |
| static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName); | |
| static WCHAR *XCOPY_LoadMessage(UINT id); | |
| static void XCOPY_FailMessage(DWORD err); | |
| static int XCOPY_wprintf(const WCHAR *format, ...); | |
| /* Typedefs */ | |
| typedef struct _EXCLUDELIST | |
| { | |
| struct _EXCLUDELIST *next; | |
| WCHAR *name; | |
| } EXCLUDELIST; | |
| /* Global variables */ | |
| static ULONG filesCopied = 0; /* Number of files copied */ | |
| static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */ | |
| static FILETIME dateRange; /* Date range to copy after*/ | |
| 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 | |
| ========================================================================= */ | |
| int wmain (int argc, WCHAR *argvW[]) | |
| { | |
| 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 */ | |
| WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */ | |
| DWORD flags = 0; /* Option flags */ | |
| const WCHAR PROMPTSTR1[] = {'/', 'Y', 0}; | |
| const WCHAR PROMPTSTR2[] = {'/', 'y', 0}; | |
| const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0}; | |
| const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0}; | |
| /* | |
| * Parse the command line | |
| */ | |
| /* Confirm at least one parameter */ | |
| if (argc < 2) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS)); | |
| return RC_INITERROR; | |
| } | |
| /* Preinitialize flags based on COPYCMD */ | |
| if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) { | |
| if (wcsstr(copyCmd, PROMPTSTR1) != NULL || | |
| wcsstr(copyCmd, PROMPTSTR2) != NULL) { | |
| flags |= OPT_NOPROMPT; | |
| } | |
| } | |
| /* 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; | |
| /* 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 { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS)); | |
| return RC_INITERROR; | |
| } | |
| } else { | |
| /* 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 */ | |
| 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; | |
| case 'W': flags |= OPT_PAUSE; break; | |
| case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break; | |
| case 'Y': flags |= OPT_NOPROMPT; break; | |
| case 'N': flags |= OPT_SHORTNAME; break; | |
| case 'U': flags |= OPT_MUSTEXIST; break; | |
| case 'R': flags |= OPT_REPLACEREAD; break; | |
| case 'H': flags |= OPT_COPYHIDSYS; break; | |
| case 'C': flags |= OPT_IGNOREERRORS; break; | |
| case 'P': flags |= OPT_SRCPROMPT; break; | |
| case 'A': flags |= OPT_ARCHIVEONLY; break; | |
| case 'M': flags |= OPT_ARCHIVEONLY | | |
| OPT_REMOVEARCH; break; | |
| /* E can be /E or /EXCLUDE */ | |
| case 'E': if (CompareStringW(LOCALE_USER_DEFAULT, | |
| NORM_IGNORECASE | SORT_STRINGSORT, | |
| &argvW[0][1], 8, | |
| EXCLUDE, -1) == 2) { | |
| if (XCOPY_ProcessExcludeList(&argvW[0][9])) { | |
| XCOPY_FailMessage(ERROR_INVALID_PARAMETER); | |
| return RC_INITERROR; | |
| } else flags |= OPT_EXCLUDELIST; | |
| } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE; | |
| break; | |
| /* 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); | |
| GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring, | |
| sizeof(datestring)/sizeof(WCHAR)); | |
| GetTimeFormatW(0, TIME_NOSECONDS, &st, | |
| NULL, timestring, sizeof(timestring)/sizeof(WCHAR)); | |
| WINE_TRACE("Date being used is: %s %s\n", | |
| wine_dbgstr_w(datestring), wine_dbgstr_w(timestring)); | |
| } else { | |
| XCOPY_FailMessage(ERROR_INVALID_PARAMETER); | |
| return RC_INITERROR; | |
| } | |
| } else { | |
| flags |= OPT_DATENEWER; | |
| } | |
| break; | |
| case '-': if (toupper(argvW[0][2])=='Y') | |
| flags &= ~OPT_NOPROMPT; break; | |
| case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP)); | |
| return RC_OK; | |
| default: | |
| WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW)); | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), *argvW); | |
| return RC_INITERROR; | |
| } | |
| } | |
| 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 */ | |
| XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags); | |
| /* Extract required information from destination specification */ | |
| 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)); | |
| /* Pause if necessary */ | |
| if (flags & OPT_PAUSE) { | |
| DWORD count; | |
| char pausestr[10]; | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE)); | |
| ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr), | |
| &count, NULL); | |
| } | |
| /* Now do the hard work... */ | |
| rc = XCOPY_DoCopy(sourcestem, sourcespec, | |
| destinationstem, destinationspec, | |
| flags); | |
| /* Clear up exclude list allocated memory */ | |
| while (excludeList) { | |
| EXCLUDELIST *pos = excludeList; | |
| excludeList = excludeList -> next; | |
| HeapFree(GetProcessHeap(), 0, pos->name); | |
| HeapFree(GetProcessHeap(), 0, pos); | |
| } | |
| /* Finished - print trailer and exit */ | |
| if (flags & OPT_SIMULATE) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied); | |
| } else if (!(flags & OPT_NOCOPY)) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied); | |
| } | |
| 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 | |
| ========================================================================= */ | |
| static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, | |
| WCHAR *spec, DWORD flags) | |
| { | |
| WCHAR actualsource[MAX_PATH]; | |
| WCHAR *starPos; | |
| WCHAR *questPos; | |
| DWORD attribs; | |
| /* | |
| * Validate the source, expanding to full path ensuring it exists | |
| */ | |
| if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) { | |
| WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError()); | |
| return RC_INITERROR; | |
| } | |
| /* If full names required, convert to using the full path */ | |
| if (flags & OPT_FULL) { | |
| lstrcpyW(suppliedsource, actualsource); | |
| } | |
| /* | |
| * Work out the stem of the source | |
| */ | |
| /* 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, '?'); | |
| if (starPos || questPos) { | |
| attribs = 0x00; /* Ensures skips invalid or directory check below */ | |
| } else { | |
| attribs = GetFileAttributesW(actualsource); | |
| } | |
| if (attribs == INVALID_FILE_ATTRIBUTES) { | |
| XCOPY_FailMessage(GetLastError()); | |
| return RC_INITERROR; | |
| /* 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; | |
| WINE_TRACE("Directory supplied\n"); | |
| lstrcpyW(stem, suppliedsource); | |
| lastChar = stem[lstrlenW(stem)-1]; | |
| if (lastChar != '\\' && lastChar != ':') { | |
| lstrcatW(stem, wchr_slash); | |
| } | |
| lstrcpyW(spec, wchr_star); | |
| /* 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); | |
| } else { | |
| WCHAR curdir[MAXSTRING]; | |
| GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir); | |
| stem[0] = curdir[0]; | |
| stem[1] = curdir[1]; | |
| stem[2] = 0x00; | |
| lstrcpyW(spec, suppliedsource); | |
| } | |
| } | |
| 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 | |
| */ | |
| if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) { | |
| WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError()); | |
| return RC_INITERROR; | |
| } | |
| /* Destination is either a directory or a file */ | |
| attribs = GetFileAttributesW(actualdestination); | |
| 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] = ""; | |
| 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)); | |
| while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination); | |
| ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL); | |
| WINE_TRACE("User answer %c\n", answer[0]); | |
| answer[0] = toupper(answer[0]); | |
| } | |
| if (answer[0] == dirChar[0]) { | |
| 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) | |
| { | |
| WIN32_FIND_DATAW *finddata; | |
| HANDLE h; | |
| BOOL findres = TRUE; | |
| WCHAR *inputpath, *outputpath; | |
| BOOL copiedFile = FALSE; | |
| DWORD destAttribs, srcAttribs; | |
| BOOL skipFile; | |
| int ret = 0; | |
| /* Allocate some working memory on heap to minimize footprint */ | |
| finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW)); | |
| 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 */ | |
| h = FindFirstFileW(inputpath, finddata); | |
| while (h != INVALID_HANDLE_VALUE && findres) { | |
| skipFile = FALSE; | |
| /* 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); | |
| if (flags & OPT_SHORTNAME) { | |
| lstrcatW(copyFrom, finddata->cAlternateFileName); | |
| } else { | |
| lstrcatW(copyFrom, finddata->cFileName); | |
| } | |
| lstrcpyW(copyTo, deststem); | |
| if (*destspec == 0x00) { | |
| if (flags & OPT_SHORTNAME) { | |
| lstrcatW(copyTo, finddata->cAlternateFileName); | |
| } else { | |
| lstrcatW(copyTo, finddata->cFileName); | |
| } | |
| } 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); | |
| /* See if allowed to copy it */ | |
| srcAttribs = GetFileAttributesW(copyFrom); | |
| WINE_TRACE("Source attribs: %d\n", srcAttribs); | |
| if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) || | |
| (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) { | |
| if (!(flags & OPT_COPYHIDSYS)) { | |
| skipFile = TRUE; | |
| } | |
| } | |
| if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) && | |
| (flags & OPT_ARCHIVEONLY)) { | |
| skipFile = TRUE; | |
| } | |
| /* See if file exists */ | |
| destAttribs = GetFileAttributesW(copyTo); | |
| WINE_TRACE("Dest attribs: %d\n", srcAttribs); | |
| /* 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)) { | |
| HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ, | |
| 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); | |
| } | |
| } | |
| /* 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); | |
| CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper)); | |
| /* 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; | |
| 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)); | |
| while (!answered) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom); | |
| ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), | |
| &count, NULL); | |
| answered = TRUE; | |
| if (toupper(answer[0]) == noChar[0]) | |
| skipFile = TRUE; | |
| else if (toupper(answer[0]) != yesChar[0]) | |
| answered = FALSE; | |
| } | |
| } | |
| if (!skipFile && | |
| destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) { | |
| DWORD count; | |
| char answer[10]; | |
| BOOL answered = FALSE; | |
| 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)); | |
| while (!answered) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo); | |
| ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), | |
| &count, NULL); | |
| answered = TRUE; | |
| if (toupper(answer[0]) == allChar[0]) | |
| flags |= OPT_NOPROMPT; | |
| else if (toupper(answer[0]) == noChar[0]) | |
| skipFile = TRUE; | |
| else if (toupper(answer[0]) != yesChar[0]) | |
| answered = FALSE; | |
| } | |
| } | |
| /* See if it has to exist! */ | |
| if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) { | |
| skipFile = TRUE; | |
| } | |
| /* Output a status message */ | |
| if (!skipFile) { | |
| if (flags & OPT_QUIET) { | |
| /* Skip message */ | |
| } else if (flags & OPT_FULL) { | |
| const WCHAR infostr[] = {'%', 's', ' ', '-', '>', ' ', | |
| '%', 's', '\n', 0}; | |
| XCOPY_wprintf(infostr, copyFrom, copyTo); | |
| } else { | |
| const WCHAR infostr[] = {'%', 's', '\n', 0}; | |
| XCOPY_wprintf(infostr, copyFrom); | |
| } | |
| /* If allowing overwriting of read only files, remove any | |
| write protection */ | |
| if ((destAttribs & FILE_ATTRIBUTE_READONLY) && | |
| (flags & OPT_REPLACEREAD)) { | |
| SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY); | |
| } | |
| copiedFile = TRUE; | |
| if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) { | |
| /* Skip copy */ | |
| } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) { | |
| DWORD error = GetLastError(); | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL), | |
| copyFrom, copyTo, error); | |
| XCOPY_FailMessage(error); | |
| if (flags & OPT_IGNOREERRORS) { | |
| skipFile = TRUE; | |
| } else { | |
| ret = RC_WRITEERROR; | |
| goto cleanup; | |
| } | |
| } | |
| /* If /M supplied, remove the archive bit after successful copy */ | |
| if (!skipFile) { | |
| if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) && | |
| (flags & OPT_REMOVEARCH)) { | |
| SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE)); | |
| } | |
| filesCopied++; | |
| } | |
| } | |
| } | |
| /* Find next file */ | |
| findres = FindNextFileW(h, finddata); | |
| } | |
| 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)); | |
| h = FindFirstFileW(inputpath, finddata); | |
| 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 */ | |
| findres = FindNextFileW(h, finddata); | |
| } | |
| } | |
| cleanup: | |
| /* free up memory */ | |
| HeapFree(GetProcessHeap(), 0, finddata); | |
| HeapFree(GetProcessHeap(), 0, inputpath); | |
| HeapFree(GetProcessHeap(), 0, outputpath); | |
| return ret; | |
| } | |
| /* ========================================================================= | |
| * Routine copied from cmd.exe md command - | |
| * This works recursively. so creating dir1\dir2\dir3 will create dir1 and | |
| * 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; | |
| while (!CreateDirectoryW(new_path,NULL)) | |
| { | |
| 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; | |
| } | |
| /* ========================================================================= | |
| * 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) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename); | |
| *endOfName = endChar; | |
| return TRUE; | |
| } | |
| /* Process line by line */ | |
| while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) { | |
| EXCLUDELIST *thisEntry; | |
| int length = lstrlenW(buffer); | |
| /* Strip CRLF */ | |
| buffer[length-1] = 0x00; | |
| /* 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); | |
| CharUpperBuffW(thisEntry->name, length); | |
| WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name)); | |
| } | |
| } | |
| /* See if EOF or error occurred */ | |
| if (!feof(inFile)) { | |
| XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename); | |
| *endOfName = endChar; | |
| return TRUE; | |
| } | |
| /* Revert the input string to original form, and cleanup + return */ | |
| *endOfName = endChar; | |
| fclose(inFile); | |
| return FALSE; | |
| } | |
| /* ========================================================================= | |
| * Load a string from the resource file, handling any error | |
| * Returns string retrieved from resource file | |
| * ========================================================================= */ | |
| static WCHAR *XCOPY_LoadMessage(UINT id) { | |
| static WCHAR msg[MAXSTRING]; | |
| const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0}; | |
| if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) { | |
| 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; | |
| status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | | |
| FORMAT_MESSAGE_FROM_SYSTEM, | |
| NULL, err, 0, | |
| (LPWSTR) &lpMsgBuf, 0, NULL); | |
| if (!status) { | |
| WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n", | |
| err, GetLastError()); | |
| } else { | |
| const WCHAR infostr[] = {'%', 's', '\n', 0}; | |
| XCOPY_wprintf(infostr, lpMsgBuf); | |
| LocalFree ((HLOCAL)lpMsgBuf); | |
| } | |
| } | |
| /* ========================================================================= | |
| * 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; | |
| DWORD nOut; | |
| int len; | |
| 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); | |
| len = vsnprintfW(output_bufW, MAX_WRITECONSOLE_SIZE/sizeof(WCHAR), format, parms); | |
| va_end(parms); | |
| if (len < 0) { | |
| WINE_FIXME("String too long.\n"); | |
| return 0; | |
| } | |
| /* 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; | |
| } |