Leaked source code of windows server 2003
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

733 lines
23 KiB

  1. /*++
  2. Copyright (c) 1998 Microsoft Corporation
  3. Module Name:
  4. extract.cpp
  5. Abstract:
  6. SIS Groveler USN journal reading functions
  7. Authors:
  8. Cedric Krumbein, 1998
  9. Environment:
  10. User Mode
  11. Revision History:
  12. --*/
  13. #include "all.hxx"
  14. // NT Update Sequence Number (USN) journal definitions
  15. #define USN_ADD_REASONS ( 0U \
  16. | USN_REASON_DATA_OVERWRITE \
  17. | USN_REASON_DATA_EXTEND \
  18. | USN_REASON_DATA_TRUNCATION \
  19. | USN_REASON_NAMED_DATA_OVERWRITE \
  20. | USN_REASON_NAMED_DATA_EXTEND \
  21. | USN_REASON_NAMED_DATA_TRUNCATION \
  22. | USN_REASON_FILE_CREATE \
  23. | USN_REASON_FILE_DELETE \
  24. /* | USN_REASON_PROPERTY_CHANGE */ \
  25. /* | USN_REASON_SECURITY_CHANGE */ \
  26. /* | USN_REASON_RENAME_OLD_NAME */ \
  27. /* | USN_REASON_RENAME_NEW_NAME */ \
  28. | USN_REASON_INDEXABLE_CHANGE \
  29. | USN_REASON_BASIC_INFO_CHANGE \
  30. /* | USN_REASON_HARD_LINK_CHANGE */ \
  31. | USN_REASON_COMPRESSION_CHANGE \
  32. | USN_REASON_ENCRYPTION_CHANGE \
  33. | USN_REASON_OBJECT_ID_CHANGE \
  34. /* | USN_REASON_REPARSE_POINT_CHANGE */ \
  35. | USN_REASON_CLOSE \
  36. )
  37. /*****************************************************************************/
  38. // set_usn_log_size() sets the maximum size of this volume's USN journal.
  39. DWORD Groveler::set_usn_log_size(
  40. IN DWORDLONG usn_log_size)
  41. {
  42. CREATE_USN_JOURNAL_DATA createUSN;
  43. DWORD transferCount;
  44. DWORD lstatus;
  45. ASSERT(volumeHandle != NULL);
  46. createUSN.MaximumSize = usn_log_size;
  47. createUSN.AllocationDelta = USN_PAGE_SIZE;
  48. // Set the maximum size of the USN journal.
  49. if (!DeviceIoControl(
  50. volumeHandle,
  51. FSCTL_CREATE_USN_JOURNAL,
  52. &createUSN,
  53. sizeof(CREATE_USN_JOURNAL_DATA),
  54. NULL,
  55. 0,
  56. &transferCount,
  57. NULL)) {
  58. lstatus = GetLastError();
  59. DPRINTF((_T("%s: error setting USN journal size: %lu\n"),
  60. driveName, lstatus));
  61. return lstatus;
  62. }
  63. TPRINTF((_T("%s: set USN journal size to %I64u\n"),
  64. driveName, usn_log_size));
  65. return ERROR_SUCCESS;
  66. }
  67. /*****************************************************************************/
  68. // get_usn_log_size() returns the current size of this volume's USN journal.
  69. DWORD Groveler::get_usn_log_info(
  70. OUT USN_JOURNAL_DATA *usnJournalData)
  71. {
  72. DWORD transferCount,
  73. lastError;
  74. BOOL success;
  75. ASSERT(volumeHandle != NULL);
  76. // Query the USN journal settings.
  77. success = DeviceIoControl(
  78. volumeHandle,
  79. FSCTL_QUERY_USN_JOURNAL,
  80. NULL,
  81. 0,
  82. usnJournalData,
  83. sizeof(USN_JOURNAL_DATA),
  84. &transferCount,
  85. NULL);
  86. if (!success)
  87. lastError = GetLastError();
  88. else if (transferCount != sizeof(USN_JOURNAL_DATA)) {
  89. lastError = ERROR_INVALID_DATA;
  90. success = FALSE;
  91. }
  92. if (!success) {
  93. DPRINTF((_T("%s: error querying USN journal settings: %lu\n"),
  94. driveName, lastError));
  95. return lastError;
  96. }
  97. TPRINTF((_T("%s: USN journal: ID=0x%I64x size=0x%I64x\n"),
  98. driveName, usnJournalData->UsnJournalID,
  99. usnJournalData->MaximumSize));
  100. return ERROR_SUCCESS;
  101. }
  102. /*****************************************************************************/
  103. // extract_log() reads this volume's USN journal.
  104. // If the lastUSN parameter equals zero or doesn't exist, the USN journal
  105. // is read from the beginning. Otherwise, the lastUSN paramerer indicates
  106. // the most recent USN entry read during the last call of extract_log().
  107. // If the lastUSN entry is still available in the USN journal, read the
  108. // journal beginning at the entry following the lastUSN entry. If the
  109. // lastUSN entry is no longer available, it indicates that the USN
  110. // journal has wrapped: read all entries from the journal.
  111. enum USNException {
  112. USN_ERROR
  113. };
  114. enum DatabaseException {
  115. DATABASE_ERROR
  116. };
  117. GrovelStatus Groveler::extract_log2(
  118. OUT DWORD *num_entries_extracted,
  119. OUT DWORDLONG *num_bytes_extracted,
  120. OUT DWORDLONG *num_bytes_skipped,
  121. OUT DWORD *num_files_enqueued,
  122. OUT DWORD *num_files_dequeued)
  123. {
  124. struct FileEntry {
  125. DWORDLONG fileID,
  126. parentID,
  127. timeStamp;
  128. DWORD attributes,
  129. reason;
  130. } *fileEntry = NULL;
  131. struct DirEntry {
  132. DWORDLONG dirID;
  133. } *dirEntry = NULL;
  134. Table *fileTable = NULL,
  135. *dirTable = NULL;
  136. BYTE usnBuffer[USN_PAGE_SIZE + sizeof(DWORDLONG)];
  137. READ_USN_JOURNAL_DATA readUSN;
  138. USN_RECORD *usnRecord;
  139. SGNativeTableEntry tableEntry;
  140. SGNativeQueueEntry queueEntry;
  141. SGNativeStackEntry stackEntry;
  142. SGNativeListEntry listEntry;
  143. TCHAR listValue[17];
  144. DWORDLONG usn_log_size,
  145. numBytesExtracted = 0,
  146. numBytesSkipped = 0,
  147. startUSN,
  148. firstUSN,
  149. nextUSN,
  150. thisUSN;
  151. DWORD numEntriesExtracted = 0,
  152. numTableDeletions = 0,
  153. numQueueDeletions = 0 ,
  154. numQueueAdditions = 0,
  155. numActions = 0,
  156. offset,
  157. bytesRead,
  158. lastError;
  159. LONG num;
  160. BOOL firstEntry = TRUE,
  161. deleteEntry,
  162. addEntry,
  163. success;
  164. GrovelStatus status;
  165. ASSERT(volumeHandle != NULL);
  166. ASSERT(sgDatabase != NULL);
  167. // If we don't know the previous USN, we can't extract.
  168. if (lastUSN == UNINITIALIZED_USN) {
  169. status = Grovel_overrun;
  170. goto Abort;
  171. }
  172. ASSERT(usnID != UNINITIALIZED_USN);
  173. fileTable = new Table;
  174. ASSERT(fileTable != NULL);
  175. if (inScan) {
  176. dirTable = new Table;
  177. ASSERT(dirTable != NULL);
  178. }
  179. // Set up to read the volume's USN journal.
  180. startUSN = lastUSN == UNINITIALIZED_USN ? 0 : lastUSN;
  181. readUSN.ReturnOnlyOnClose = 1;
  182. readUSN.Timeout = 0;
  183. readUSN.BytesToWaitFor = 0;
  184. readUSN.ReasonMask = ~0U;
  185. readUSN.UsnJournalID = usnID;
  186. // Read the USN journal one page at a time.
  187. try {
  188. while (TRUE) {
  189. readUSN.StartUsn = startUSN;
  190. if (!DeviceIoControl(
  191. volumeHandle,
  192. FSCTL_READ_USN_JOURNAL,
  193. &readUSN,
  194. sizeof(READ_USN_JOURNAL_DATA),
  195. usnBuffer,
  196. USN_PAGE_SIZE + sizeof(DWORDLONG),
  197. &bytesRead,
  198. NULL)) {
  199. lastError = GetLastError();
  200. // NTRAID#65198-2000/03/10-nealch Handle USN id change (treat as overwrite w/ unknown no. of bytes skipped)
  201. // If the journal overflowed, report by how much.
  202. if (lastError == ERROR_KEY_DELETED || lastError == ERROR_JOURNAL_ENTRY_DELETED) {
  203. USN_JOURNAL_DATA usnJournalData;
  204. if (get_usn_log_info(&usnJournalData) != ERROR_SUCCESS)
  205. return Grovel_error;
  206. // The USN journal will not wrap in our lifetimes so we don't really need
  207. // to handle USN Journal wrapping.
  208. ASSERT((DWORDLONG) usnJournalData.FirstUsn > lastUSN);
  209. numBytesSkipped = (DWORDLONG) usnJournalData.FirstUsn - lastUSN;
  210. goto Overrun;
  211. }
  212. throw USN_ERROR;
  213. }
  214. lastError = 0;
  215. if (bytesRead < sizeof(DWORDLONG))
  216. throw USN_ERROR;
  217. nextUSN = *(DWORDLONG *)usnBuffer;
  218. if (nextUSN < startUSN)
  219. throw USN_ERROR;
  220. if (nextUSN == startUSN) {
  221. if (bytesRead != sizeof(DWORDLONG))
  222. throw USN_ERROR;
  223. break;
  224. }
  225. bytesRead -= sizeof(DWORDLONG);
  226. offset = 0;
  227. numBytesExtracted += bytesRead;
  228. // Process each USN journal entry.
  229. while (bytesRead > 0) {
  230. if (bytesRead < sizeof(USN_RECORD))
  231. throw USN_ERROR;
  232. usnRecord = (USN_RECORD *)&usnBuffer[offset + sizeof(DWORDLONG)];
  233. if (usnRecord->RecordLength <
  234. offsetof(USN_RECORD, FileName) + usnRecord->FileNameLength
  235. || usnRecord->RecordLength > bytesRead)
  236. throw USN_ERROR;
  237. thisUSN = (DWORDLONG)usnRecord->Usn;
  238. if (thisUSN < startUSN + offset)
  239. throw USN_ERROR;
  240. // If this is the first entry, check if it is the expected
  241. // USN. If it isn't, the USN journal has wrapped.
  242. if (firstEntry)
  243. if (startUSN == 0)
  244. numBytesSkipped = thisUSN;
  245. else if (thisUSN <= startUSN + usnRecord->RecordLength)
  246. numBytesSkipped = 0;
  247. else
  248. numBytesSkipped = thisUSN - startUSN - usnRecord->RecordLength;
  249. // Skip the first entry if the starting address is greater than zero.
  250. // After skipping the first entry, examine each USN entry as follows:
  251. //
  252. // - If the entry is a directory, and a volume scan is underway,
  253. // add the directory's ID to the directory table.
  254. //
  255. // - If the entry is a file, add it to the file table. Include
  256. // its ID and its parent directory's ID, its most recent time
  257. // stamp and attributes, and its accumulated reason bits.
  258. if (firstEntry && startUSN > 0)
  259. numBytesExtracted -= usnRecord->RecordLength;
  260. else {
  261. if (usnRecord-> FileReferenceNumber == 0
  262. || usnRecord->ParentFileReferenceNumber == 0)
  263. throw USN_ERROR;
  264. // The entry is a directory.
  265. if ((usnRecord->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
  266. if (dirTable != NULL) {
  267. dirEntry = (DirEntry *)dirTable->Get(
  268. (const VOID *)&usnRecord->FileReferenceNumber,
  269. sizeof(DWORDLONG));
  270. if (dirEntry != NULL) {
  271. ASSERT(dirEntry->dirID == usnRecord->FileReferenceNumber);
  272. } else {
  273. dirEntry = new DirEntry;
  274. ASSERT(dirEntry != NULL);
  275. dirEntry->dirID = usnRecord->FileReferenceNumber;
  276. success = dirTable->Put((VOID *)dirEntry, sizeof(DWORDLONG));
  277. ASSERT(success);
  278. }
  279. }
  280. }
  281. // The entry is a file. If USN_SOURCE_DATA_MANAGEMENT is set, assume this entry was created by
  282. // the groveler during a merge operation.
  283. else if ((usnRecord->SourceInfo & USN_SOURCE_DATA_MANAGEMENT) == 0) {
  284. fileEntry = (FileEntry *)fileTable->Get(
  285. (const VOID *)&usnRecord->FileReferenceNumber,
  286. sizeof(DWORDLONG));
  287. if (fileEntry != NULL) {
  288. ASSERT(fileEntry->fileID == usnRecord->FileReferenceNumber);
  289. } else {
  290. fileEntry = new FileEntry;
  291. ASSERT(fileEntry != NULL);
  292. fileEntry->fileID = usnRecord->FileReferenceNumber;
  293. fileEntry->reason = 0;
  294. success = fileTable->Put((VOID *)fileEntry, sizeof(DWORDLONG));
  295. ASSERT(success);
  296. }
  297. fileEntry->parentID = usnRecord->ParentFileReferenceNumber;
  298. fileEntry->timeStamp = (DWORDLONG)usnRecord->TimeStamp.QuadPart;
  299. fileEntry->attributes = usnRecord->FileAttributes;
  300. if ((usnRecord->Reason & USN_REASON_FILE_DELETE) != 0)
  301. fileEntry->reason = USN_REASON_FILE_DELETE;
  302. else
  303. fileEntry->reason |= usnRecord->Reason;
  304. } else {
  305. TPRINTF((_T("%s: USN_SOURCE_DATA_MANAGEMENT set on file 0x%016I64x\n"),
  306. driveName, usnRecord->FileReferenceNumber));
  307. }
  308. if (numEntriesExtracted++ == 0)
  309. firstUSN = thisUSN;
  310. }
  311. lastUSN = thisUSN;
  312. offset += usnRecord->RecordLength;
  313. bytesRead -= usnRecord->RecordLength;
  314. firstEntry = FALSE;
  315. }
  316. startUSN = nextUSN;
  317. }
  318. }
  319. // If an error occured while reading the USN journal, return an error status.
  320. catch (USNException usnException) {
  321. ASSERT(usnException == USN_ERROR);
  322. if (fileTable != NULL) {
  323. delete fileTable;
  324. fileTable = NULL;
  325. }
  326. if (dirTable != NULL) {
  327. delete dirTable;
  328. dirTable = NULL;
  329. }
  330. lastUSN = UNINITIALIZED_USN;
  331. DPRINTF((_T("%s: error reading USN journal: %lu\n"),
  332. driveName, lastError));
  333. return Grovel_error;
  334. }
  335. // We've finished reading the USN journal, so update the database. Process
  336. // each entry in the file table, and group the updates into transactions.
  337. try {
  338. while ((fileEntry = (FileEntry *)fileTable->GetFirst()) != NULL) {
  339. ASSERT(fileEntry->fileID != 0);
  340. // If the file is currently open in the grovel process, skip this entry.
  341. if (inUseFileID1 != NULL && fileEntry->fileID == *inUseFileID1
  342. || inUseFileID2 != NULL && fileEntry->fileID == *inUseFileID2) {
  343. DPRINTF((_T("%s: extract_log/grovel collision on file 0x%016I64x\n"),
  344. driveName, fileEntry->fileID));
  345. } else {
  346. // Delete the file from the queue and the table...
  347. //
  348. // - if the file's most recent reason bits in the USN journal
  349. // indicate it was deleted,
  350. //
  351. // - if the file or the file's most recent parent directory is disallowed,
  352. //
  353. // - or if the file has disallowed attributes.
  354. //
  355. // Otherwise, update or add the file to the queue...
  356. //
  357. // - if the file's reason bits indicate it was changed,
  358. //
  359. // - or if the file isn't present in the table.
  360. if (fileEntry->reason == USN_REASON_FILE_DELETE
  361. || !IsAllowedID(fileEntry->fileID)
  362. || !IsAllowedID(fileEntry->parentID)
  363. || (fileEntry->attributes & disallowedAttributes) != 0) {
  364. deleteEntry = TRUE;
  365. addEntry = FALSE;
  366. } else {
  367. deleteEntry = FALSE;
  368. if ((fileEntry->reason & USN_ADD_REASONS) != 0)
  369. addEntry = TRUE;
  370. else {
  371. tableEntry.fileID = fileEntry->fileID;
  372. num = sgDatabase->TableGetFirstByFileID(&tableEntry);
  373. if (num < 0)
  374. throw DATABASE_ERROR;
  375. ASSERT(num == 0 || num == 1);
  376. addEntry = num == 0;
  377. }
  378. }
  379. if (deleteEntry || addEntry) {
  380. if (numActions == 0) {
  381. if (sgDatabase->BeginTransaction() < 0)
  382. throw DATABASE_ERROR;
  383. numActions = 1;
  384. }
  385. queueEntry.reason = 0;
  386. num = sgDatabase->TableDeleteByFileID(fileEntry->fileID);
  387. if (num < 0)
  388. throw DATABASE_ERROR;
  389. if (num > 0) {
  390. ASSERT(num == 1);
  391. numTableDeletions++;
  392. numActions++;
  393. }
  394. queueEntry.fileID = fileEntry->fileID;
  395. queueEntry.fileName = NULL;
  396. num = sgDatabase->QueueGetFirstByFileID(&queueEntry);
  397. if (num < 0)
  398. throw DATABASE_ERROR;
  399. if (num > 0) {
  400. ASSERT(num == 1);
  401. num = sgDatabase->QueueDeleteByFileID(fileEntry->fileID);
  402. if (num < 0)
  403. throw DATABASE_ERROR;
  404. ASSERT(num == 1);
  405. numQueueDeletions++;
  406. numActions++;
  407. }
  408. if (addEntry) {
  409. queueEntry.fileID = fileEntry->fileID;
  410. queueEntry.parentID = 0;
  411. queueEntry.reason |= fileEntry->reason;
  412. queueEntry.readyTime = fileEntry->timeStamp + minFileAge;
  413. queueEntry.retryTime = 0;
  414. queueEntry.fileName = NULL;
  415. num = sgDatabase->QueuePut(&queueEntry);
  416. if (num < 0)
  417. throw DATABASE_ERROR;
  418. ASSERT(num == 1);
  419. #ifdef DEBUG_USN_REASON
  420. if (numQueueAdditions == 0) {
  421. DPRINTF((_T("--> __REASON__ _____FILE_ID______\n")));
  422. }
  423. DPRINTF((_T(" 0x%08lx 0x%016I64x\n"),
  424. fileEntry->reason, fileEntry->fileID));
  425. #endif
  426. numQueueAdditions++;
  427. numActions++;
  428. }
  429. if (numActions >= MAX_ACTIONS_PER_TRANSACTION) {
  430. if (!sgDatabase->CommitTransaction())
  431. throw DATABASE_ERROR;
  432. TPRINTF((_T("%s: committing %lu actions to \"%s\"\n"),
  433. driveName, numActions, databaseName));
  434. numActions = 0;
  435. }
  436. }
  437. }
  438. delete fileEntry;
  439. fileEntry = NULL;
  440. }
  441. delete fileTable;
  442. fileTable = NULL;
  443. // Process each entry in the directory table. If the directory hasn't already
  444. // been scanned or isn't on the list to be scanned, add it to the list.
  445. if (dirTable != NULL) {
  446. ASSERT(inScan);
  447. while ((dirEntry = (DirEntry *)dirTable->GetFirst()) != NULL) {
  448. ASSERT(dirEntry->dirID != 0);
  449. stackEntry.fileID = dirEntry->dirID;
  450. num = sgDatabase->StackGetFirstByFileID(&stackEntry);
  451. if (num < 0)
  452. throw DATABASE_ERROR;
  453. if (num == 0) {
  454. if (numActions == 0) {
  455. if (sgDatabase->BeginTransaction() < 0)
  456. throw DATABASE_ERROR;
  457. numActions = 1;
  458. }
  459. num = sgDatabase->StackPut(dirEntry->dirID, FALSE);
  460. if (num < 0)
  461. throw DATABASE_ERROR;
  462. ASSERT(num == 1);
  463. numActions++;
  464. if (numActions >= MAX_ACTIONS_PER_TRANSACTION) {
  465. if (!sgDatabase->CommitTransaction())
  466. throw DATABASE_ERROR;
  467. TPRINTF((_T("%s: committing %lu actions to \"%s\"\n"),
  468. driveName, numActions, databaseName));
  469. numActions = 0;
  470. }
  471. }
  472. delete dirEntry;
  473. dirEntry = NULL;
  474. }
  475. delete dirTable;
  476. dirTable = NULL;
  477. }
  478. // Update the last USN number in the database, then commit the changes. If we're
  479. // doing a volume scan, don't update the lastUSN until the scan is complete.
  480. if (!inScan) {
  481. (void)StringCbPrintf(listValue, sizeof(listValue), _T("%016I64x"), lastUSN);
  482. listEntry.name = LAST_USN_NAME;
  483. listEntry.value = listValue;
  484. num = sgDatabase->ListWrite(&listEntry);
  485. if (num <= 0)
  486. throw DATABASE_ERROR;
  487. }
  488. if (numActions > 0) {
  489. if (!sgDatabase->CommitTransaction())
  490. throw DATABASE_ERROR;
  491. TPRINTF((_T("%s: committing %lu actions to \"%s\"\n"),
  492. driveName, numActions, databaseName));
  493. numActions = 0;
  494. }
  495. }
  496. // If a database error occured, return an error status.
  497. catch (DatabaseException databaseException) {
  498. ASSERT(databaseException == DATABASE_ERROR);
  499. if (numActions > 0) {
  500. sgDatabase->AbortTransaction();
  501. numActions = 0;
  502. }
  503. if (fileTable != NULL) {
  504. delete fileTable;
  505. fileTable = NULL;
  506. }
  507. if (dirTable != NULL) {
  508. delete dirTable;
  509. dirTable = NULL;
  510. }
  511. return Grovel_error;
  512. }
  513. Overrun:
  514. status = numBytesSkipped == 0 ? Grovel_ok : Grovel_overrun;
  515. Abort:
  516. // Return the performance statistics.
  517. if (num_entries_extracted != NULL)
  518. *num_entries_extracted = numEntriesExtracted;
  519. if (num_bytes_extracted != NULL)
  520. *num_bytes_extracted = numBytesExtracted;
  521. if (num_bytes_skipped != NULL)
  522. *num_bytes_skipped = numBytesSkipped;
  523. if (num_files_enqueued != NULL)
  524. *num_files_enqueued = numQueueAdditions;
  525. if (num_files_dequeued != NULL)
  526. *num_files_dequeued = numQueueDeletions;
  527. #if DBG
  528. if (numEntriesExtracted > 0 && firstUSN < lastUSN) {
  529. TRACE_PRINTF(TC_extract, 2,
  530. (_T("%s: USN 0x%I64x-%I64x\n"), driveName, firstUSN, lastUSN));
  531. } else {
  532. TRACE_PRINTF(TC_extract, 2,
  533. (_T("%s: USN 0x%I64x\n"), driveName, lastUSN));
  534. }
  535. TRACE_PRINTF(TC_extract, 2,
  536. (_T(" NumEntriesExtracted=%lu NumBytesExtracted=%I64u NumBytesSkipped=%I64u\n"),
  537. numEntriesExtracted, numBytesExtracted, numBytesSkipped));
  538. TRACE_PRINTF(TC_extract, 2,
  539. (_T(" NumTableDeletions=%lu NumQueueDeletions=%lu NumQueueAdditions=%lu\n"),
  540. numTableDeletions, numQueueDeletions, numQueueAdditions));
  541. #endif
  542. return status;
  543. }
  544. GrovelStatus Groveler::extract_log(
  545. OUT DWORD *num_entries_extracted,
  546. OUT DWORDLONG *num_bytes_extracted,
  547. OUT DWORDLONG *num_bytes_skipped,
  548. OUT DWORD *num_files_enqueued,
  549. OUT DWORD *num_files_dequeued)
  550. {
  551. GrovelStatus status;
  552. #ifdef _CRTDBG
  553. _CrtMemState s1, s2, sdiff;
  554. _CrtMemCheckpoint(&s1);
  555. #endif
  556. status = extract_log2(
  557. num_entries_extracted,
  558. num_bytes_extracted,
  559. num_bytes_skipped,
  560. num_files_enqueued,
  561. num_files_dequeued);
  562. #ifdef _CRTDBG
  563. _CrtMemCheckpoint(&s2);
  564. if (_CrtMemDifference(&sdiff, &s1, &s2))
  565. _CrtMemDumpStatistics(&sdiff);
  566. #endif
  567. return status;
  568. }