Source code of Windows XP (NT5)
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.

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