anshdadhich commited on
Commit
b299dd7
·
verified ·
1 Parent(s): f55dd69

Upload FastSeekWpf/Core/MftReader.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/MftReader.cs +126 -222
FastSeekWpf/Core/MftReader.cs CHANGED
@@ -3,7 +3,6 @@ using System.Buffers.Binary;
3
  using System.Collections.Generic;
4
  using System.IO;
5
  using System.Runtime.InteropServices;
6
- using System.Security.Principal;
7
  using FastSeekWpf.NativeInterop;
8
 
9
  namespace FastSeekWpf.Core;
@@ -12,218 +11,148 @@ public class MftReader : IDisposable
12
  {
13
  private readonly IntPtr _volumeHandle;
14
  private readonly NtfsDrive _drive;
15
- private readonly int _recordSize;
16
- private readonly long _mftByteOffset;
17
  private bool _disposed;
18
 
19
- private const int FallbackBuf = 4 * 1024 * 1024;
20
- private const int DirectBuf = 4 * 1024 * 1024;
21
 
22
  public NtfsDrive Drive => _drive;
23
 
24
  public MftReader(NtfsDrive drive)
25
  {
26
  _drive = drive;
27
- _volumeHandle = Win32Api.CreateFileW(
28
- drive.DevicePath,
29
- Win32Api.GENERIC_READ,
30
- Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
31
- IntPtr.Zero,
32
- Win32Api.OPEN_EXISTING,
33
- Win32Api.FILE_FLAG_BACKUP_SEMANTICS,
34
- IntPtr.Zero);
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  if (_volumeHandle == new IntPtr(-1))
37
  {
38
  int err = Marshal.GetLastWin32Error();
39
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
40
  }
41
-
42
- // Read boot sector once and cache record size + MFT offset
43
- var bootInfo = ReadBootSector();
44
- if (!bootInfo.HasValue)
45
- throw new IOException("Failed to read NTFS boot sector");
46
-
47
- _recordSize = bootInfo.Value.recordSize;
48
- _mftByteOffset = bootInfo.Value.mftOffset;
49
  }
50
 
51
  // ------------------------------------------------------------------
52
- // Scan methods — try in order: $MFT file volume raw FSCTL
53
  // ------------------------------------------------------------------
54
 
55
- /// <summary>
56
- /// Try all scan methods and return the first that works.
57
- /// </summary>
58
- public ScanResult ScanAny()
59
  {
60
- // 1. Try $MFT file (fastest, matches Rust)
61
- var direct = ScanDirect();
62
- if (direct != null && direct.Records.Count > 0)
63
- {
64
- Logger.Log($"[{Drive.Letter}] Using direct $MFT file scan: {direct.Records.Count} records");
65
- return direct;
66
- }
67
 
68
- // 2. Try reading raw from volume handle
69
- var vol = ScanDirectVolume();
70
- if (vol != null && vol.Records.Count > 0)
71
- {
72
- Logger.Log($"[{Drive.Letter}] Using volume raw scan: {vol.Records.Count} records");
73
- return vol;
74
- }
75
 
76
- // 3. FSCTL fallback (slowest but most compatible)
77
- var fallback = Scan();
78
- if (fallback.Records.Count > 0)
79
  {
80
- Logger.Log($"[{Drive.Letter}] Using FSCTL fallback scan: {fallback.Records.Count} records");
81
- return fallback;
 
 
82
  }
83
 
84
- Logger.Log($"[{Drive.Letter}] ALL scan methods returned 0 records");
85
- return fallback;
86
- }
87
-
88
- /// <summary>
89
- /// Direct $MFT file read. Matches Rust scan_direct().
90
- /// </summary>
91
- public ScanResult? ScanDirect()
92
- {
93
- string mftPath = $"{_drive.Root}$MFT";
94
- Logger.Log($"[{Drive.Letter}] Opening $MFT file: {mftPath}");
95
-
96
- IntPtr mftHandle = Win32Api.CreateFileW(
97
- mftPath,
98
- Win32Api.GENERIC_READ,
99
- Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
100
- IntPtr.Zero,
101
- Win32Api.OPEN_EXISTING,
102
- Win32Api.FILE_FLAG_BACKUP_SEMANTICS | Win32Api.FILE_FLAG_SEQUENTIAL_SCAN,
103
- IntPtr.Zero);
104
-
105
  if (mftHandle == new IntPtr(-1))
106
  {
107
  int err = Marshal.GetLastWin32Error();
108
- Logger.Log($"[{Drive.Letter}] CreateFileW({mftPath}) failed: {Win32Error(err)} (code={err})");
109
  return null;
110
  }
111
 
112
  try
113
  {
114
- var result = ScanFromHandle(mftHandle, "direct-file");
115
- return result;
116
- }
117
- finally
118
- {
119
- Win32Api.CloseHandle(mftHandle);
120
- }
121
- }
122
 
123
- /// <summary>
124
- /// Read MFT records directly from the volume handle at the boot-sector offset.
125
- /// Fallback when $MFT file is blocked.
126
- /// </summary>
127
- public ScanResult? ScanDirectVolume()
128
- {
129
- Logger.Log($"[{Drive.Letter}] Attempting volume raw read at offset {_mftByteOffset}...");
130
-
131
- bool ok = Win32Api.SetFilePointerEx(_volumeHandle, _mftByteOffset, out _, 0);
132
- if (!ok)
133
- {
134
- int err = Marshal.GetLastWin32Error();
135
- Logger.Log($"[{Drive.Letter}] Seek to MFT offset failed: {Win32Error(err)} (code={err})");
136
- return null;
137
- }
138
 
139
- try
140
- {
141
- var result = ScanFromHandle(_volumeHandle, "direct-volume");
142
- return result;
143
- }
144
- catch (Exception ex)
145
- {
146
- Logger.Log($"[{Drive.Letter}] Volume raw read failed: {ex.Message}");
147
- return null;
148
- }
149
- }
150
 
151
- /// <summary>
152
- /// Common scan loop used by both direct-file and direct-volume.
153
- /// </summary>
154
- private ScanResult ScanFromHandle(IntPtr handle, string methodName)
155
- {
156
- var records = new List<CompactRecord>();
157
- var nameData = new List<char>();
158
- byte[] buffer = new byte[DirectBuf];
159
- ulong mftIndex = 0;
160
- int leftover = 0;
161
- long totalRead = 0;
162
 
163
- while (true)
164
- {
165
- bool ok = Win32Api.ReadFile(
166
- handle, buffer, leftover, (uint)(buffer.Length - leftover), out uint bytesRead, IntPtr.Zero);
167
- if (!ok)
168
- {
169
- int err = Marshal.GetLastWin32Error();
170
- if (err == 38) // ERROR_HANDLE_EOF
171
  {
172
- Logger.Log($"[{Drive.Letter}] {methodName}: EOF at offset {totalRead}");
173
- break;
 
 
 
 
 
 
 
 
 
 
174
  }
175
- Logger.Log($"[{Drive.Letter}] {methodName}: ReadFile failed at offset {totalRead}: {Win32Error(err)} (code={err})");
176
- break;
177
- }
178
- if (bytesRead == 0)
179
- {
180
- Logger.Log($"[{Drive.Letter}] {methodName}: ReadFile returned 0 bytes at offset {totalRead} — EOF");
181
- break;
182
- }
183
 
184
- totalRead += bytesRead;
185
- int total = leftover + (int)bytesRead;
186
- int offset = 0;
187
 
188
- while (offset + _recordSize <= total)
189
- {
190
- bool applied = ApplyFixup(buffer, offset, _recordSize);
191
- if (applied)
192
  {
193
- ParseFileRecord(buffer.AsSpan(offset, _recordSize), mftIndex, records, nameData);
 
194
  }
195
- mftIndex++;
196
- offset += _recordSize;
197
  }
198
 
199
- offset = total - (total % _recordSize);
200
- leftover = total - offset;
201
- if (leftover > 0)
202
- Array.Copy(buffer, offset, buffer, 0, leftover);
203
-
204
- if (mftIndex % 50000 == 0)
205
- Logger.Log($"[{Drive.Letter}] {methodName}: {mftIndex:N0} records parsed, {records.Count:N0} valid");
206
  }
207
-
208
- Logger.Log($"[{Drive.Letter}] {methodName}: {records.Count} records from {mftIndex} positions, {totalRead:N0} bytes read");
209
- return new ScanResult(records, nameData);
210
  }
211
 
212
- /// <summary>
213
- /// FSCTL_ENUM_USN_DATA fallback.
214
- /// </summary>
215
- public ScanResult Scan()
216
  {
217
- Logger.Log($"[{Drive.Letter}] Starting FSCTL_ENUM_USN_DATA fallback...");
218
- Logger.Log($"[{Drive.Letter}] MFT_ENUM_DATA_V0 size = {Marshal.SizeOf<MFT_ENUM_DATA_V0>()} bytes");
219
- Logger.Log($"[{Drive.Letter}] USN_RECORD_V2 size = {Marshal.SizeOf<USN_RECORD_V2>()} bytes");
 
 
 
 
 
 
220
 
221
- // Diagnostic: test FSCTL_QUERY_USN_JOURNAL
222
- TestUsnJournal();
 
223
 
 
 
224
  var records = new List<CompactRecord>();
225
  var nameData = new List<char>();
226
- byte[] buffer = new byte[FallbackBuf];
227
 
228
  var enumData = new MFT_ENUM_DATA_V0
229
  {
@@ -245,28 +174,33 @@ public class MftReader : IDisposable
245
  IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length);
246
  try
247
  {
 
248
  bool ok = Win32Api.DeviceIoControl(
249
- _volumeHandle, Win32Api.FSCTL_ENUM_USN_DATA,
250
- enumDataPtr, (uint)enumDataSize,
251
- bufferPtr, (uint)buffer.Length,
252
- out uint bytesReturned,
 
 
 
253
  IntPtr.Zero);
254
 
255
  if (!ok)
256
  {
257
  int error = Marshal.GetLastWin32Error();
258
- if ((uint)error == 0x26) // ERROR_HANDLE_EOF
 
259
  {
260
  Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
261
  break;
262
  }
263
  if (error == 122) // ERROR_INSUFFICIENT_BUFFER
264
  {
265
- Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: buffer too small ({buffer.Length}), retrying");
266
  buffer = new byte[buffer.Length * 2];
267
  continue;
268
  }
269
- Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)} (code={error})");
270
  break;
271
  }
272
 
@@ -281,13 +215,15 @@ public class MftReader : IDisposable
281
  Marshal.StructureToPtr(enumData, enumDataPtr, false);
282
 
283
  int offset = 8;
284
- while (offset + Marshal.SizeOf<USN_RECORD_V2>() <= (int)bytesReturned)
 
285
  {
286
  IntPtr recordPtr = IntPtr.Add(bufferPtr, offset);
287
  var record = Marshal.PtrToStructure<USN_RECORD_V2>(recordPtr);
288
 
289
  int recLen = (int)record.RecordLength;
290
- if (recLen == 0 || offset + recLen > (int)bytesReturned) break;
 
291
 
292
  int nameOffset = offset + (int)record.FileNameOffset;
293
  int nameLen = record.FileNameLength / 2;
@@ -327,74 +263,38 @@ public class MftReader : IDisposable
327
  }
328
 
329
  // ------------------------------------------------------------------
330
- // Diagnostics
331
  // ------------------------------------------------------------------
332
 
333
- private void TestUsnJournal()
334
  {
335
- Logger.Log($"[{Drive.Letter}] Diagnostic: testing FSCTL_QUERY_USN_JOURNAL...");
336
- IntPtr journalBuf = Marshal.AllocHGlobal(Marshal.SizeOf<USN_JOURNAL_DATA_V0>());
337
- try
338
- {
339
- bool ok = Win32Api.DeviceIoControl(
340
- _volumeHandle, Win32Api.FSCTL_QUERY_USN_JOURNAL,
341
- IntPtr.Zero, 0,
342
- journalBuf, (uint)Marshal.SizeOf<USN_JOURNAL_DATA_V0>(),
343
- out uint bytesReturned, IntPtr.Zero);
344
- if (ok)
345
- {
346
- var journalData = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(journalBuf);
347
- Logger.Log($"[{Drive.Letter}] USN journal OK: ID={journalData.UsnJournalID}, NextUsn={journalData.NextUsn}");
348
- }
349
- else
350
- {
351
- int err = Marshal.GetLastWin32Error();
352
- Logger.Log($"[{Drive.Letter}] FSCTL_QUERY_USN_JOURNAL failed: {Win32Error(err)} (code={err})");
353
- }
354
- }
355
- finally
356
- {
357
- Marshal.FreeHGlobal(journalBuf);
358
- }
359
- }
360
-
361
- // ------------------------------------------------------------------
362
- // NTFS helpers
363
- // ------------------------------------------------------------------
364
-
365
- private (int recordSize, long mftOffset)? ReadBootSector()
366
- {
367
- Logger.Log($"[{Drive.Letter}] Reading boot sector from volume...");
368
  Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
369
 
370
  byte[] boot = new byte[512];
371
- bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out uint br, IntPtr.Zero);
 
372
  if (!ok || br < 512)
373
- {
374
- int err = Marshal.GetLastWin32Error();
375
- Logger.Log($"[{Drive.Letter}] Boot sector read failed: {Win32Error(err)} (code={err})");
376
  return null;
377
- }
378
 
379
  if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
380
- {
381
- Logger.Log($"[{Drive.Letter}] Not NTFS (missing signature at offset 3)");
382
  return null;
383
- }
384
 
385
  ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2));
386
  byte sectorsPerCluster = boot[0x0D];
387
  int clusterSize = bytesPerSector * sectorsPerCluster;
388
 
389
- long mftStartLcn = BinaryPrimitives.ReadInt64LittleEndian(boot.AsSpan(0x30, 8));
390
  sbyte raw = (sbyte)boot[0x40];
391
  int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw);
392
- long mftOffset = mftStartLcn * clusterSize;
393
 
394
- Logger.Log($"[{Drive.Letter}] Boot OK: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}, mftLcn={mftStartLcn}, mftOffset={mftOffset}");
395
- return (recordSize, mftOffset);
396
  }
397
 
 
 
 
 
398
  private static bool ApplyFixup(byte[] record, int offset, int recordSize)
399
  {
400
  if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I'
@@ -418,6 +318,7 @@ public class MftReader : IDisposable
418
  if (record[offset + end] != check0 || record[offset + end + 1] != check1)
419
  return false;
420
 
 
421
  record[offset + end] = record[offset + fixupOff + i * 2];
422
  record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
423
  }
@@ -425,6 +326,9 @@ public class MftReader : IDisposable
425
  return true;
426
  }
427
 
 
 
 
428
  private static void ParseFileRecord(
429
  ReadOnlySpan<byte> record,
430
  ulong mftIndex,
@@ -432,7 +336,7 @@ public class MftReader : IDisposable
432
  List<char> nameData)
433
  {
434
  ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x16, 2));
435
- if ((flags & 0x01) == 0) return;
436
 
437
  bool isDir = (flags & 0x02) != 0;
438
  ushort seq = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x10, 2));
@@ -452,7 +356,7 @@ public class MftReader : IDisposable
452
  uint alen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 4, 4));
453
  if (alen == 0 || aoff + alen > record.Length) break;
454
 
455
- if (atype == 0x30 && record[aoff + 8] == 0)
456
  {
457
  uint vlen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 16, 4));
458
  ushort voff = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(aoff + 20, 2));
@@ -466,7 +370,7 @@ public class MftReader : IDisposable
466
 
467
  if (vs + 66 + nlen * 2 <= record.Length)
468
  {
469
- if (ns == 2)
470
  {
471
  aoff += (int)alen;
472
  continue;
@@ -474,9 +378,9 @@ public class MftReader : IDisposable
474
 
475
  byte priority = ns switch
476
  {
477
- 1 => 0,
478
- 3 => 1,
479
- 0 => 2,
480
  _ => 3,
481
  };
482
 
 
3
  using System.Collections.Generic;
4
  using System.IO;
5
  using System.Runtime.InteropServices;
 
6
  using FastSeekWpf.NativeInterop;
7
 
8
  namespace FastSeekWpf.Core;
 
11
  {
12
  private readonly IntPtr _volumeHandle;
13
  private readonly NtfsDrive _drive;
 
 
14
  private bool _disposed;
15
 
16
+ private const int FALLBACK_BUF = 4 * 1024 * 1024;
17
+ private const int DIRECT_BUF = 4 * 1024 * 1024;
18
 
19
  public NtfsDrive Drive => _drive;
20
 
21
  public MftReader(NtfsDrive drive)
22
  {
23
  _drive = drive;
24
+
25
+ // Open volume device for boot sector read (matches Rust MftReader::open exactly)
26
+ var pathBytes = System.Text.Encoding.Unicode.GetBytes(drive.DevicePath + '\0');
27
+ GCHandle pathHandle = GCHandle.Alloc(pathBytes, GCHandleType.Pinned);
28
+ try
29
+ {
30
+ _volumeHandle = Win32Api.CreateFileW(
31
+ drive.DevicePath,
32
+ Win32Api.GENERIC_READ,
33
+ Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
34
+ IntPtr.Zero,
35
+ Win32Api.OPEN_EXISTING,
36
+ Win32Api.FILE_FLAG_BACKUP_SEMANTICS,
37
+ IntPtr.Zero);
38
+ }
39
+ finally
40
+ {
41
+ pathHandle.Free();
42
+ }
43
 
44
  if (_volumeHandle == new IntPtr(-1))
45
  {
46
  int err = Marshal.GetLastWin32Error();
47
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
48
  }
 
 
 
 
 
 
 
 
49
  }
50
 
51
  // ------------------------------------------------------------------
52
+ // Primary: direct $MFT file read (matches Rust scan_direct exactly)
53
  // ------------------------------------------------------------------
54
 
55
+ public ScanResult? ScanDirect()
 
 
 
56
  {
57
+ int? recordSize = ReadMftRecordSize();
58
+ if (!recordSize.HasValue) return null;
 
 
 
 
 
59
 
60
+ string mftPath = $"{_drive.Root}$MFT";
61
+ Logger.Log($"[{Drive.Letter}] Opening $MFT: {mftPath}");
 
 
 
 
 
62
 
63
+ IntPtr mftHandle = OpenMftFile(mftPath);
64
+ if (mftHandle == new IntPtr(-1))
 
65
  {
66
+ // Try extended-length path prefix as fallback
67
+ string extendedPath = $"\\\\?\\{mftPath}";
68
+ Logger.Log($"[{Drive.Letter}] Retrying with extended path: {extendedPath}");
69
+ mftHandle = OpenMftFile(extendedPath);
70
  }
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  if (mftHandle == new IntPtr(-1))
73
  {
74
  int err = Marshal.GetLastWin32Error();
75
+ Logger.Log($"[{Drive.Letter}] CreateFileW($MFT) failed: {Win32Error(err)} (code={err})");
76
  return null;
77
  }
78
 
79
  try
80
  {
81
+ var records = new List<CompactRecord>();
82
+ var nameData = new List<char>();
83
+ byte[] buffer = new byte[DIRECT_BUF];
84
+ ulong mftIndex = 0;
85
+ int leftover = 0;
 
 
 
86
 
87
+ while (true)
88
+ {
89
+ uint bytesRead = 0;
90
+ bool ok = Win32Api.ReadFile(
91
+ mftHandle, buffer, leftover, (uint)(buffer.Length - leftover), out bytesRead, IntPtr.Zero);
 
 
 
 
 
 
 
 
 
 
92
 
93
+ if (!ok || bytesRead == 0)
94
+ break;
 
 
 
 
 
 
 
 
 
95
 
96
+ int total = leftover + (int)bytesRead;
97
+ int offset = 0;
 
 
 
 
 
 
 
 
 
98
 
99
+ while (offset + recordSize.Value <= total)
 
 
 
 
 
 
 
100
  {
101
+ bool applied = ApplyFixup(buffer, offset, recordSize.Value);
102
+ if (applied)
103
+ {
104
+ ParseFileRecord(
105
+ buffer.AsSpan(offset, recordSize.Value),
106
+ mftIndex,
107
+ records,
108
+ nameData);
109
+ }
110
+
111
+ mftIndex += 1;
112
+ offset += recordSize.Value;
113
  }
 
 
 
 
 
 
 
 
114
 
115
+ // Align down to record boundary (matches Rust: offset = total - (total % record_size))
116
+ offset = total - (total % recordSize.Value);
 
117
 
118
+ leftover = total - offset;
119
+ if (leftover > 0)
 
 
120
  {
121
+ // Copy tail to front of buffer (matches Rust std::ptr::copy)
122
+ Buffer.BlockCopy(buffer, offset, buffer, 0, leftover);
123
  }
 
 
124
  }
125
 
126
+ Logger.Log($"[{Drive.Letter}] Direct scan: {records.Count} records from {mftIndex} positions");
127
+ return new ScanResult(records, nameData);
128
+ }
129
+ finally
130
+ {
131
+ Win32Api.CloseHandle(mftHandle);
 
132
  }
 
 
 
133
  }
134
 
135
+ private static IntPtr OpenMftFile(string path)
 
 
 
136
  {
137
+ return Win32Api.CreateFileW(
138
+ path,
139
+ Win32Api.GENERIC_READ,
140
+ Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
141
+ IntPtr.Zero,
142
+ Win32Api.OPEN_EXISTING,
143
+ Win32Api.FILE_FLAG_BACKUP_SEMANTICS | Win32Api.FILE_FLAG_SEQUENTIAL_SCAN,
144
+ IntPtr.Zero);
145
+ }
146
 
147
+ // ------------------------------------------------------------------
148
+ // Fallback: FSCTL_ENUM_USN_DATA (matches Rust scan exactly)
149
+ // ------------------------------------------------------------------
150
 
151
+ public ScanResult Scan()
152
+ {
153
  var records = new List<CompactRecord>();
154
  var nameData = new List<char>();
155
+ byte[] buffer = new byte[FALLBACK_BUF];
156
 
157
  var enumData = new MFT_ENUM_DATA_V0
158
  {
 
174
  IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length);
175
  try
176
  {
177
+ uint bytesReturned = 0;
178
  bool ok = Win32Api.DeviceIoControl(
179
+ _volumeHandle,
180
+ Win32Api.FSCTL_ENUM_USN_DATA,
181
+ enumDataPtr,
182
+ (uint)enumDataSize,
183
+ bufferPtr,
184
+ (uint)buffer.Length,
185
+ out bytesReturned,
186
  IntPtr.Zero);
187
 
188
  if (!ok)
189
  {
190
  int error = Marshal.GetLastWin32Error();
191
+ uint code = (uint)error;
192
+ if (code == 0x80070026) // ERROR_HANDLE_EOF
193
  {
194
  Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
195
  break;
196
  }
197
  if (error == 122) // ERROR_INSUFFICIENT_BUFFER
198
  {
199
+ Logger.Log($"[{Drive.Letter}] FSCTL buffer too small, retrying with {buffer.Length * 2}");
200
  buffer = new byte[buffer.Length * 2];
201
  continue;
202
  }
203
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed: {Win32Error(error)} (code={error})");
204
  break;
205
  }
206
 
 
215
  Marshal.StructureToPtr(enumData, enumDataPtr, false);
216
 
217
  int offset = 8;
218
+ int usnRecordSize = Marshal.SizeOf<USN_RECORD_V2>();
219
+ while (offset + usnRecordSize <= (int)bytesReturned)
220
  {
221
  IntPtr recordPtr = IntPtr.Add(bufferPtr, offset);
222
  var record = Marshal.PtrToStructure<USN_RECORD_V2>(recordPtr);
223
 
224
  int recLen = (int)record.RecordLength;
225
+ if (recLen == 0 || offset + recLen > (int)bytesReturned)
226
+ break;
227
 
228
  int nameOffset = offset + (int)record.FileNameOffset;
229
  int nameLen = record.FileNameLength / 2;
 
263
  }
264
 
265
  // ------------------------------------------------------------------
266
+ // NTFS helpers (match Rust exactly)
267
  // ------------------------------------------------------------------
268
 
269
+ private int? ReadMftRecordSize()
270
  {
271
+ // Seek to start of volume
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
273
 
274
  byte[] boot = new byte[512];
275
+ uint br = 0;
276
+ bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out br, IntPtr.Zero);
277
  if (!ok || br < 512)
 
 
 
278
  return null;
 
279
 
280
  if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
 
 
281
  return null;
 
282
 
283
  ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2));
284
  byte sectorsPerCluster = boot[0x0D];
285
  int clusterSize = bytesPerSector * sectorsPerCluster;
286
 
 
287
  sbyte raw = (sbyte)boot[0x40];
288
  int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw);
 
289
 
290
+ Logger.Log($"[{Drive.Letter}] Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}");
291
+ return recordSize;
292
  }
293
 
294
+ /// <summary>
295
+ /// Apply NTFS multi-sector fixup. Returns false if record is invalid.
296
+ /// CRITICAL: restores bytes from fixup array back to sector ends (matches Rust exactly).
297
+ /// </summary>
298
  private static bool ApplyFixup(byte[] record, int offset, int recordSize)
299
  {
300
  if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I'
 
318
  if (record[offset + end] != check0 || record[offset + end + 1] != check1)
319
  return false;
320
 
321
+ // RESTORE real bytes from fixup array (this is what Rust does)
322
  record[offset + end] = record[offset + fixupOff + i * 2];
323
  record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
324
  }
 
326
  return true;
327
  }
328
 
329
+ /// <summary>
330
+ /// Parse one MFT FILE record (matches Rust parse_file_record exactly).
331
+ /// </summary>
332
  private static void ParseFileRecord(
333
  ReadOnlySpan<byte> record,
334
  ulong mftIndex,
 
336
  List<char> nameData)
337
  {
338
  ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x16, 2));
339
+ if ((flags & 0x01) == 0) return; // Record not in use
340
 
341
  bool isDir = (flags & 0x02) != 0;
342
  ushort seq = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x10, 2));
 
356
  uint alen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 4, 4));
357
  if (alen == 0 || aoff + alen > record.Length) break;
358
 
359
+ if (atype == 0x30 && record[aoff + 8] == 0) // $FILE_NAME, resident
360
  {
361
  uint vlen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 16, 4));
362
  ushort voff = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(aoff + 20, 2));
 
370
 
371
  if (vs + 66 + nlen * 2 <= record.Length)
372
  {
373
+ if (ns == 2) // DOS 8.3 name — skip
374
  {
375
  aoff += (int)alen;
376
  continue;
 
378
 
379
  byte priority = ns switch
380
  {
381
+ 1 => 0, // Win32
382
+ 3 => 1, // Win32 + DOS
383
+ 0 => 2, // POSIX
384
  _ => 3,
385
  };
386