/* * 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: * Documented valid return codes are: * 0 - OK * 1 - No files found to copy (*1) * 2 - CTRL+C during copy * 4 - Initialization error, or invalid source specification * 5 - Disk write error * * (*1) Testing shows return code 1 is never returned */ #include <stdio.h> #include <stdlib.h> #include <windows.h> #include <wine/debug.h> #include "xcopy.h" WINE_DEFAULT_DEBUG_CHANNEL(xcopy); /* 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*/ /* To minimize stack usage during recursion, some temporary variables made global */ static WCHAR copyFrom[MAX_PATH]; static WCHAR copyTo[MAX_PATH]; /* ========================================================================= * 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]; if (!LoadStringW(GetModuleHandleW(NULL), id, msg, ARRAY_SIZE(msg))) { WINE_FIXME("LoadString failed with %d\n", GetLastError()); lstrcpyW(msg, L"Failed!"); } return msg; } /* ========================================================================= * 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 * ========================================================================= */ static int WINAPIV 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 __ms_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*sizeof(WCHAR)); if (!output_bufW) { WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n"); return 0; } __ms_va_start(parms, format); len = FormatMessageW(FORMAT_MESSAGE_FROM_STRING, format, 0, 0, output_bufW, MAX_WRITECONSOLE_SIZE/sizeof(*output_bufW), &parms); __ms_va_end(parms); if (len == 0 && GetLastError() != ERROR_NO_WORK_DONE) { WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format)); return 0; } /* Try to write as unicode whenever we think it's a console */ if (toConsole) { res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), output_bufW, len, &nOut, NULL); } /* If writing to console has failed (ever) we assume it's 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; } /* ========================================================================= * 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 { XCOPY_wprintf(L"%1\n", lpMsgBuf); LocalFree ((HLOCAL)lpMsgBuf); } } /* ========================================================================= * 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 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; /* Null terminate the filename (temporarily updates the filename hence parms not const) */ *endOfName = 0x00; /* Open the file */ inFile = _wfopen(filename, L"rt"); if (inFile == NULL) { XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename); *endOfName = endChar; return TRUE; } /* Process line by line */ while (fgetws(buffer, ARRAY_SIZE(buffer), inFile) != NULL) { EXCLUDELIST *thisEntry; int length = lstrlenW(buffer); /* If more than CRLF */ if (length > 1) { buffer[length-1] = 0; /* strip CRLF */ 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; fclose(inFile); return TRUE; } /* Revert the input string to original form, and cleanup + return */ *endOfName = endChar; fclose(inFile); return FALSE; } /* ========================================================================= * 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; } /* ========================================================================= 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, L".")==0 || lstrcmpW(finddata->cFileName, L"..")==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)) { if (flags & OPT_FULL) XCOPY_wprintf(L"%1 -> %2\n", copyFrom, copyTo); else XCOPY_wprintf(L"%1\n", 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; } } else { if (!skipFile) { /* If keeping attributes, update the destination attributes otherwise remove the read only attribute */ if (flags & OPT_KEEPATTRS) { SetFileAttributesW(copyTo, srcAttribs | FILE_ATTRIBUTE_ARCHIVE); } else { SetFileAttributesW(copyTo, (GetFileAttributesW(copyTo) & ~FILE_ATTRIBUTE_READONLY)); } /* If /M supplied, remove the archive bit after successful copy */ 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) { /* If /E is supplied, create the directory now */ if ((flags & OPT_EMPTYDIR) && !(flags & OPT_SIMULATE)) { XCOPY_CreateDirectory(deststem); } lstrcpyW(inputpath, srcstem); lstrcatW(inputpath, L"*"); 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, L".") != 0) && (lstrcmpW(finddata->cFileName, L"..") != 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, L"\\"); lstrcpyW(outputpath, deststem); if (*destspec == 0x00) { lstrcatW(outputpath, finddata->cFileName); lstrcatW(outputpath, L"\\"); } XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags); } /* Find next one */ findres = FindNextFileW(h, finddata); } FindClose(h); } cleanup: /* free up memory */ HeapFree(GetProcessHeap(), 0, finddata); HeapFree(GetProcessHeap(), 0, inputpath); HeapFree(GetProcessHeap(), 0, outputpath); return ret; } /* ========================================================================= XCOPY_ParseCommandLine - Parses the command line ========================================================================= */ static inline BOOL is_whitespace(WCHAR c) { return c == ' ' || c == '\t'; } static WCHAR *skip_whitespace(WCHAR *p) { for (; *p && is_whitespace(*p); p++); return p; } static inline BOOL is_digit(WCHAR c) { return c >= '0' && c <= '9'; } /* Windows XCOPY uses a simplified command line parsing algorithm that lacks the escaped-quote logic of build_argv(), because literal double quotes are illegal in any of its arguments. Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */ static int find_end_of_word(const WCHAR *word, WCHAR **end) { BOOL in_quotes = FALSE; const WCHAR *ptr = word; for (;;) { for (; *ptr != '\0' && *ptr != '"' && (in_quotes || !is_whitespace(*ptr)); ptr++); if (*ptr == '"') { in_quotes = !in_quotes; ptr++; } /* Odd number of double quotes is illegal for XCOPY */ if (in_quotes && *ptr == '\0') return RC_INITERROR; if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr))) break; } *end = (WCHAR*)ptr; return RC_OK; } /* Remove all double quotes from a word */ static void strip_quotes(WCHAR *word, WCHAR **end) { WCHAR *rp, *wp; for (rp = word, wp = word; *rp != '\0'; rp++) { if (*rp == '"') continue; if (wp < rp) *wp = *rp; wp++; } *wp = '\0'; *end = wp; } static int XCOPY_ParseCommandLine(WCHAR *suppliedsource, WCHAR *supplieddestination, DWORD *pflags) { DWORD flags = *pflags; WCHAR *cmdline, *word, *end, *next; int rc = RC_INITERROR; cmdline = _wcsdup(GetCommandLineW()); if (cmdline == NULL) return rc; /* Skip first arg, which is the program name */ if ((rc = find_end_of_word(cmdline, &word)) != RC_OK) goto out; word = skip_whitespace(word); while (*word) { WCHAR first; if ((rc = find_end_of_word(word, &end)) != RC_OK) goto out; next = skip_whitespace(end); first = word[0]; *end = '\0'; strip_quotes(word, &end); WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word)); /* First non-switch parameter is source, second is destination */ if (first != '/') { if (suppliedsource[0] == 0x00) { lstrcpyW(suppliedsource, word); } else if (supplieddestination[0] == 0x00) { lstrcpyW(supplieddestination, word); } else { XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS)); goto out; } } 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 */ int skip=0; WCHAR *rest; while (word[0]) { rest = NULL; switch (toupper(word[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 'K': flags |= OPT_KEEPATTRS; 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, &word[1], 8, L"EXCLUDE:", -1) == CSTR_EQUAL) { if (XCOPY_ProcessExcludeList(&word[9])) { XCOPY_FailMessage(ERROR_INVALID_PARAMETER); goto out; } else { flags |= OPT_EXCLUDELIST; /* Do not support concatenated switches onto exclude lists yet */ rest = end; } } else { flags |= OPT_EMPTYDIR | OPT_RECURSIVE; } break; /* D can be /D or /D: */ case 'D': if (word[2]==':' && is_digit(word[3])) { SYSTEMTIME st; WCHAR *pos = &word[3]; BOOL isError = FALSE; memset(&st, 0x00, sizeof(st)); /* Microsoft xcopy's usage message implies that the date * format depends on the locale, but that is false. * It is hardcoded to month-day-year. */ st.wMonth = _wtol(pos); while (*pos && is_digit(*pos)) pos++; if (*pos++ != '-') isError = TRUE; if (!isError) { st.wDay = _wtol(pos); while (*pos && is_digit(*pos)) pos++; if (*pos++ != '-') isError = TRUE; } if (!isError) { st.wYear = _wtol(pos); while (*pos && is_digit(*pos)) pos++; if (st.wYear < 100) st.wYear+=2000; } /* Handle switches straight after the supplied date */ rest = pos; 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, ARRAY_SIZE(datestring)); GetTimeFormatW(0, TIME_NOSECONDS, &st, NULL, timestring, ARRAY_SIZE(timestring)); WINE_TRACE("Date being used is: %s %s\n", wine_dbgstr_w(datestring), wine_dbgstr_w(timestring)); } else { XCOPY_FailMessage(ERROR_INVALID_PARAMETER); goto out; } } else { flags |= OPT_DATENEWER; } break; case '-': if (toupper(word[2])=='Y') { flags &= ~OPT_NOPROMPT; rest = &word[3]; /* Skip over 3 characters */ } break; case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP)); rc = RC_HELP; goto out; case 'V': WINE_FIXME("ignoring /V\n"); break; default: WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word)); XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word); goto out; } /* Unless overridden above, skip over the '/' and the first character */ if (rest == NULL) rest = &word[2]; /* By now, rest should point either to the null after the switch, or the beginning of the next switch if there was no whitespace between them */ if (!skip && *rest && *rest != '/') { WINE_FIXME("Unexpected characters found and ignored '%s'\n", wine_dbgstr_w(rest)); skip=1; } else { word = rest; } } } word = next; } /* Default the destination if not supplied */ if (supplieddestination[0] == 0x00) lstrcpyW(supplieddestination, L"."); *pflags = flags; rc = RC_OK; out: free(cmdline); 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, L"\\"); lstrcpyW(spec, L"*"); /* 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(ARRAY_SIZE(curdir), 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 */ /* Also if destination ends with backslash */ if ((flags & OPT_ASSUMEDIR && (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) || (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) { 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, L"\\"); } 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; } /* ========================================================================= main - Main entrypoint for the xcopy command Processes the args, and drives the actual copying ========================================================================= */ int __cdecl 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 */ /* Preinitialize flags based on COPYCMD */ if (GetEnvironmentVariableW(L"COPYCMD", copyCmd, MAXSTRING)) { if (wcsstr(copyCmd, L"/Y") != NULL || wcsstr(copyCmd, L"/y") != 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; /* * Parse the command line */ if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination, &flags)) != RC_OK) { if (rc == RC_HELP) return RC_OK; else return rc; } /* 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 */ rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags); if (rc != RC_OK) return rc; /* Extract required information from destination specification */ rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem, destinationspec, sourcespec, flags); if (rc != RC_OK) return rc; /* 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); } return rc; }