xcopy.c 42 KB

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