using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using FastSeekWpf.NativeInterop; namespace FastSeekWpf.Core; public class MftReader : IDisposable { private readonly IntPtr _volumeHandle; private readonly NtfsDrive _drive; private bool _disposed; private const int FALLBACK_BUF = 4 * 1024 * 1024; private const int DIRECT_BUF = 4 * 1024 * 1024; public NtfsDrive Drive => _drive; public MftReader(NtfsDrive drive) { _drive = drive; _volumeHandle = Win32Api.CreateFileW( drive.DevicePath, Win32Api.GENERIC_READ, Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE, IntPtr.Zero, Win32Api.OPEN_EXISTING, Win32Api.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (_volumeHandle == new IntPtr(-1)) { int err = Marshal.GetLastWin32Error(); throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})"); } } // ------------------------------------------------------------------ // Try direct first, fall back to FSCTL (matches Rust main.rs exactly) // ------------------------------------------------------------------ public (ScanResult scan, string method) ScanAny() { var direct = ScanDirect(); if (direct != null) return (direct, "direct"); return (Scan(), "ioctl"); } // ------------------------------------------------------------------ // Primary: direct $MFT file read (matches Rust scan_direct()) // ------------------------------------------------------------------ public ScanResult? ScanDirect() { int? recordSize = ReadMftRecordSize(); if (!recordSize.HasValue) return null; string mftPath = $"{_drive.Root}$MFT"; Logger.Log($"[{Drive.Letter}] Opening $MFT: {mftPath}"); IntPtr mftHandle = Win32Api.CreateFileW( mftPath, Win32Api.GENERIC_READ, Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE, IntPtr.Zero, Win32Api.OPEN_EXISTING, Win32Api.FILE_FLAG_BACKUP_SEMANTICS | Win32Api.FILE_FLAG_SEQUENTIAL_SCAN, IntPtr.Zero); if (mftHandle == new IntPtr(-1)) { int err = Marshal.GetLastWin32Error(); Logger.Log($"[{Drive.Letter}] CreateFileW({mftPath}) failed: {Win32Error(err)} (code={err})"); return null; } try { var records = new List(); var nameData = new List(); byte[] buffer = new byte[DIRECT_BUF]; ulong mftIndex = 0; int leftover = 0; while (true) { uint bytesRead = 0; bool ok = Win32Api.ReadFile( mftHandle, buffer, leftover, (uint)(buffer.Length - leftover), out bytesRead, IntPtr.Zero); if (!ok || bytesRead == 0) break; int total = leftover + (int)bytesRead; int offset = 0; while (offset + recordSize.Value <= total) { bool applied = ApplyFixup(buffer, offset, recordSize.Value); if (applied) { ParseFileRecord( buffer.AsSpan(offset, recordSize.Value), mftIndex, records, nameData); } mftIndex += 1; offset += recordSize.Value; } // Align down to record boundary (matches Rust: offset = total - (total % record_size)) offset = total - (total % recordSize.Value); leftover = total - offset; if (leftover > 0) { // Copy tail to front of buffer (matches Rust std::ptr::copy) Buffer.BlockCopy(buffer, offset, buffer, 0, leftover); } } Logger.Log($"[{Drive.Letter}] Direct scan: {records.Count} records from {mftIndex} positions"); return new ScanResult(records, nameData); } finally { Win32Api.CloseHandle(mftHandle); } } // ------------------------------------------------------------------ // Fallback: FSCTL_ENUM_USN_DATA (4 MB buffer) // Matches Rust scan() exactly. // ------------------------------------------------------------------ public ScanResult Scan() { var records = new List(); var nameData = new List(); byte[] buffer = new byte[FALLBACK_BUF]; var enumData = new MFT_ENUM_DATA_V0 { StartFileReferenceNumber = 0, LowUsn = 0, HighUsn = long.MaxValue }; int enumDataSize = Marshal.SizeOf(); IntPtr enumDataPtr = Marshal.AllocHGlobal(enumDataSize); Marshal.StructureToPtr(enumData, enumDataPtr, false); try { int iterations = 0; while (true) { iterations++; IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length); try { uint bytesReturned = 0; bool ok = Win32Api.DeviceIoControl( _volumeHandle, Win32Api.FSCTL_ENUM_USN_DATA, enumDataPtr, (uint)enumDataSize, bufferPtr, (uint)buffer.Length, out bytesReturned, IntPtr.Zero); if (!ok) { int error = Marshal.GetLastWin32Error(); uint code = (uint)error; if (code == 0x80070026) // ERROR_HANDLE_EOF { Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations"); break; } if (error == 122) // ERROR_INSUFFICIENT_BUFFER { Logger.Log($"[{Drive.Letter}] FSCTL buffer too small ({buffer.Length}), retrying"); buffer = new byte[buffer.Length * 2]; continue; } Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed: {Win32Error(error)} (code={error})"); break; } if (bytesReturned <= 8) break; ulong nextRef = (ulong)Marshal.ReadInt64(bufferPtr); enumData.StartFileReferenceNumber = nextRef; Marshal.StructureToPtr(enumData, enumDataPtr, false); int offset = 8; int usnRecordSize = Marshal.SizeOf(); while (offset + usnRecordSize <= (int)bytesReturned) { IntPtr recordPtr = IntPtr.Add(bufferPtr, offset); var record = Marshal.PtrToStructure(recordPtr); int recLen = (int)record.RecordLength; if (recLen == 0 || offset + recLen > (int)bytesReturned) break; int nameOffset = offset + (int)record.FileNameOffset; int nameLen = record.FileNameLength / 2; var nameChars = new char[nameLen]; for (int i = 0; i < nameLen; i++) nameChars[i] = (char)Marshal.ReadInt16(bufferPtr, nameOffset + i * 2); int arenaOff = nameData.Count; nameData.AddRange(nameChars); records.Add(new CompactRecord { FileRef = record.FileReferenceNumber, ParentRef = record.ParentFileReferenceNumber, NameOff = (uint)arenaOff, NameLen = (ushort)nameLen, IsDir = (record.FileAttributes & 0x10) != 0 }); offset += recLen; } } finally { Marshal.FreeHGlobal(bufferPtr); } } } finally { Marshal.FreeHGlobal(enumDataPtr); } Logger.Log($"[{Drive.Letter}] FSCTL scan: {records.Count} records"); return new ScanResult(records, nameData); } // ------------------------------------------------------------------ // NTFS helpers (match Rust exactly) // ------------------------------------------------------------------ private int? ReadMftRecordSize() { // Seek to beginning of volume Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0); byte[] boot = new byte[512]; uint br = 0; bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out br, IntPtr.Zero); if (!ok || br < 512) return null; if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S') return null; ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2)); byte sectorsPerCluster = boot[0x0D]; int clusterSize = bytesPerSector * sectorsPerCluster; sbyte raw = (sbyte)boot[0x40]; int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw); Logger.Log($"[{Drive.Letter}] Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}"); return recordSize; } /// /// Apply NTFS multi-sector fixup. Returns false if record is invalid. /// CRITICAL: restores bytes from fixup array back to sector ends (matches Rust exactly). /// private static bool ApplyFixup(byte[] record, int offset, int recordSize) { if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I' || record[offset + 2] != 'L' || record[offset + 3] != 'E') return false; ushort fixupOff = BinaryPrimitives.ReadUInt16LittleEndian(record.AsSpan(offset + 4, 2)); ushort fixupCnt = BinaryPrimitives.ReadUInt16LittleEndian(record.AsSpan(offset + 6, 2)); if (fixupCnt < 2 || fixupOff + fixupCnt * 2 > recordSize) return false; byte check0 = record[offset + fixupOff]; byte check1 = record[offset + fixupOff + 1]; for (int i = 1; i < fixupCnt; i++) { int end = i * 512 - 2; if (end + 1 >= recordSize) break; if (record[offset + end] != check0 || record[offset + end + 1] != check1) return false; // RESTORE real bytes from fixup array (this is what Rust does) record[offset + end] = record[offset + fixupOff + i * 2]; record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1]; } return true; } /// /// Parse one MFT FILE record (matches Rust parse_file_record exactly). /// private static void ParseFileRecord( ReadOnlySpan record, ulong mftIndex, List records, List nameData) { ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x16, 2)); if ((flags & 0x01) == 0) return; // Record not in use bool isDir = (flags & 0x02) != 0; ushort seq = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x10, 2)); ulong fileRef = mftIndex | ((ulong)seq << 48); ushort firstAttr = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x14, 2)); int aoff = firstAttr; byte bestNs = 255; (int pos, int len, ulong parent)? bestName = null; while (aoff + 8 <= record.Length) { uint atype = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff, 4)); if (atype == 0xFFFFFFFF) break; uint alen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 4, 4)); if (alen == 0 || aoff + alen > record.Length) break; if (atype == 0x30 && record[aoff + 8] == 0) // $FILE_NAME, resident { uint vlen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 16, 4)); ushort voff = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(aoff + 20, 2)); int vs = aoff + voff; if (vs + 66 <= record.Length && vlen >= 66) { ulong parent = BinaryPrimitives.ReadUInt64LittleEndian(record.Slice(vs, 8)); int nlen = record[vs + 64]; byte ns = record[vs + 65]; if (vs + 66 + nlen * 2 <= record.Length) { if (ns == 2) // DOS 8.3 name — skip { aoff += (int)alen; continue; } byte priority = ns switch { 1 => 0, // Win32 3 => 1, // Win32 + DOS 0 => 2, // POSIX _ => 3, }; if (priority < bestNs) { bestNs = priority; bestName = (vs + 66, nlen, parent); if (priority == 0) break; } } } } aoff += (int)alen; } if (bestName.HasValue) { var (namePos, nlen, parent) = bestName.Value; int arenaOff = nameData.Count; for (int i = 0; i < nlen; i++) { int p = namePos + i * 2; nameData.Add((char)BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(p, 2))); } records.Add(new CompactRecord { FileRef = fileRef, ParentRef = parent, NameOff = (uint)arenaOff, NameLen = (ushort)nlen, IsDir = isDir }); } } private static string Win32Error(int code) { try { return new System.ComponentModel.Win32Exception(code).Message; } catch { return $"error {code} (0x{code:X})"; } } public void Dispose() { if (!_disposed) { Win32Api.CloseHandle(_volumeHandle); _disposed = true; } GC.SuppressFinalize(this); } ~MftReader() => Dispose(); } public class CompactRecord { public ulong FileRef { get; set; } public ulong ParentRef { get; set; } public uint NameOff { get; set; } public ushort NameLen { get; set; } public bool IsDir { get; set; } } public class ScanResult { public List Records { get; } public List NameData { get; } public ScanResult(List records, List nameData) { Records = records; NameData = nameData; } }