using System.Buffers.Binary; using Microsoft.Win32.SafeHandles; namespace FluidExplorer.Services.Search.Usn; /// 从一条 $MFT 文件记录里解出来的关键字段。 internal struct MftRecordInfo { internal bool InUse; internal bool IsDirectory; internal long Size; // 数据实大小;目录或未找到 $DATA 时为 -1 internal long ModifiedFileTime; // $STANDARD_INFORMATION 偏移 8(LastDataChangeTime) internal long CreatedFileTime; // $STANDARD_INFORMATION 偏移 0 internal uint Attributes; internal ulong ParentRecordNumber; // $FILE_NAME 的父目录记录号(低 48 位) internal int NameLength; // 字符数;-1 表示没有 $FILE_NAME internal int NameRecordOffset; // 名字在记录内的字节偏移 internal byte NameNamespace; // 0=POSIX 1=Win32 2=DOS 3=Win32&DOS } /// /// 原始 $MFT 读取器:绕过 USN 日志,直接读卷上的 MFT 数据来拿“真实文件大小 / 精确时间戳 / 真实属性”。 /// /// 步骤: /// 1) FSCTL_GET_NTFS_VOLUME_DATA 拿 BytesPerSector / BytesPerCluster / BytesPerFileRecordSegment / MftStartLcn; /// 2) 直接按字节偏移读 MFT 的第 0 条记录($MFT 自身),做 fixup 修正后解析它的 $DATA(0x80) 属性 run list; /// 3) 由 run list 得到 $MFT 在卷上的全部簇区间,之后按区间批量 ReadFile 并逐条做 fixup 修正 + 属性解析。 /// /// 权限:ReadFile 卷句柄需要 GENERIC_READ,即必须有管理员权限。拿不到时由调用方降级为纯 USN 索引。 /// internal sealed class MftReader { private const int ReadBlockSize = 4 * 1024 * 1024; private const uint AttrStandardInformation = 0x10; private const uint AttrFileName = 0x30; private const uint AttrData = 0x80; private const uint AttrEnd = 0xFFFFFFFF; private const uint FileRecordSignature = 0x454C4946; // "FILE" private readonly SafeFileHandle _volume; private readonly int _bytesPerRecord; private readonly int _bytesPerSector; private readonly int _bytesPerCluster; private readonly (long Start, long Length)[] _extents; internal long MftValidDataLength { get; } internal int BytesPerRecord => _bytesPerRecord; internal int BytesPerSector => _bytesPerSector; /// MFT 在卷上的簇区间(已按字节换算),可用于日志与自检。 internal IReadOnlyList<(long Start, long Length)> Extents => _extents; private MftReader(SafeFileHandle volume, int bytesPerRecord, int bytesPerSector, int bytesPerCluster, (long, long)[] extents, long mftValidDataLength) { _volume = volume; _bytesPerRecord = bytesPerRecord; _bytesPerSector = bytesPerSector; _bytesPerCluster = bytesPerCluster; _extents = extents; MftValidDataLength = mftValidDataLength; } /// /// 尝试建立 MftReader。失败(非 NTFS、无权限、run list 解析不出来)返回 null 并通过 说明原因。 /// internal static MftReader? TryCreate(SafeFileHandle volume, in UsnNative.NtfsVolumeDataBuffer vd, out string? error) { error = null; if (vd.BytesPerSector is < 256 or > 65536) { error = $"扇区尺寸异常({vd.BytesPerSector} 字节)"; return null; } if (vd.BytesPerCluster is < 256 or > 8 * 1024 * 1024) { error = $"簇尺寸异常({vd.BytesPerCluster} 字节)"; return null; } if (vd.BytesPerFileRecordSegment is < 256 or > 65536 || vd.BytesPerFileRecordSegment % vd.BytesPerSector != 0) { error = $"MFT 记录尺寸异常({vd.BytesPerFileRecordSegment} 字节)"; return null; } int bytesPerSector = (int)vd.BytesPerSector; int bytesPerCluster = (int)vd.BytesPerCluster; int bytesPerRecord = (int)vd.BytesPerFileRecordSegment; // ---- 1) 读 $MFT 自己的记录(记录号 0),位置 = MftStartLcn 个簇 ---- var record0 = new byte[bytesPerRecord]; long mftStartByte = vd.MftStartLcn * bytesPerCluster; if (UsnNative.ReadAt(volume, record0, mftStartByte) != bytesPerRecord) { error = "无法读取 $MFT 的第一条记录(卷句柄缺少读数据权限,或磁盘未就绪)"; return null; } var reader = new MftReader(volume, bytesPerRecord, bytesPerSector, bytesPerCluster, [], vd.MftValidDataLength); if (!ApplyFixup(record0, bytesPerSector)) { error = "$MFT 记录 0 的 fixup(USA) 校验失败"; return null; } // ---- 2) 解析 $DATA(0x80) 的 run list,得到 MFT 在卷上的簇区间 ---- List<(long, long)> extents = []; if (reader.TryReadDataRuns(record0, extents) && extents.Count > 0) { return new MftReader(volume, bytesPerRecord, bytesPerSector, bytesPerCluster, [.. extents], vd.MftValidDataLength); } // ---- 3) 降级:绝大多数卷上 $MFT 是连续的,直接按 MftStartLcn 连续读 ---- long needed = vd.MftValidDataLength > 0 ? vd.MftValidDataLength : vd.NumberSectors * bytesPerSector; long span = (needed + bytesPerCluster - 1) / bytesPerCluster * bytesPerCluster; extents.Clear(); extents.Add((mftStartByte, span)); error = "$MFT 的 run list 解析失败,已按“MFT 连续存放”的假设降级读取"; return new MftReader(volume, bytesPerRecord, bytesPerSector, bytesPerCluster, [.. extents], vd.MftValidDataLength); } // ================================================================ fixup / USA /// /// 应用 NTFS 的 fixup(Update Sequence Array)修正: /// 每个扇区最后 2 个字节在磁盘上被换成了 USA 里的“更新序列号”, /// 必须在解析前用 USA 中保存的真实值逐个还原,否则跨扇区的字段会是垃圾数据。 /// 返回 false 表示 USN 不匹配(记录在读取过程中被改写或已损坏),调用方应跳过该记录。 /// 设计成 static 是为了能被“合成记录”单元测试直接调用(无需真实卷句柄)。 /// internal static bool ApplyFixup(Span record, int bytesPerSector) { if (record.Length < 8 || bytesPerSector <= 0) return false; int usaOffset = BinaryPrimitives.ReadUInt16LittleEndian(record[4..]); int usaCount = BinaryPrimitives.ReadUInt16LittleEndian(record[6..]); if (usaCount < 1) return false; if (usaOffset + usaCount * 2 > record.Length) return false; // USA 覆盖的扇区数必须与记录尺寸一致 if ((usaCount - 1) * bytesPerSector > record.Length) return false; ushort usn = BinaryPrimitives.ReadUInt16LittleEndian(record[usaOffset..]); for (int i = 1; i < usaCount; i++) { int pos = i * bytesPerSector - 2; if (pos + 2 > record.Length) return false; if (BinaryPrimitives.ReadUInt16LittleEndian(record[pos..]) != usn) return false; ushort real = BinaryPrimitives.ReadUInt16LittleEndian(record[(usaOffset + i * 2)..]); BinaryPrimitives.WriteUInt16LittleEndian(record[pos..], real); } return true; } // ================================================================ 属性解析 /// /// 从记录里读取 $DATA(0x80) 的 mapping pairs(run list),换算成卷内字节区间。 /// 这里刻意手写属性链循环而不用委托回调:该路径在构建期会被调用百万次,闭包分配不可接受。 /// private bool TryReadDataRuns(ReadOnlySpan record, List<(long, long)> extents) { if (record.Length < 48) return false; int offset = BinaryPrimitives.ReadUInt16LittleEndian(record[20..]); int guard = 0; while (offset >= 24 && offset + 8 <= record.Length && guard++ < 1024) { uint type = BinaryPrimitives.ReadUInt32LittleEndian(record[offset..]); if (type == AttrEnd) break; uint length = BinaryPrimitives.ReadUInt32LittleEndian(record[(offset + 4)..]); if (length < 24 || offset + length > record.Length) return false; bool nonResident = record[offset + 8] != 0; if (type != AttrData || !nonResident) { offset += (int)length; continue; } int runOffset = BinaryPrimitives.ReadUInt16LittleEndian(record[(offset + 32)..]); int end = offset + (int)length; if (runOffset <= 0 || offset + runOffset >= end) return false; long lcn = 0; int p = offset + runOffset; while (p < end) { int header = record[p++]; if (header == 0) break; int lenSize = header & 0x0F; int offSize = header >> 4; if (lenSize == 0 || p + lenSize + offSize > end) break; long runLength = 0; for (int i = 0; i < lenSize; i++) runLength |= (long)record[p + i] << (8 * i); p += lenSize; long delta = 0; bool sparse = offSize == 0; if (!sparse) { // 偏移字段是相对上一个 LCN 的“有符号”小端整数,必须做符号扩展 for (int i = 0; i < offSize; i++) delta |= (long)record[p + i] << (8 * i); long signBit = 1L << (8 * offSize - 1); if ((delta & signBit) != 0) delta -= signBit << 1; p += offSize; lcn += delta; } // 稀疏区段(offset 字段宽度为 0)不占物理簇,直接跳过 if (runLength > 0 && !sparse) extents.Add((lcn * _bytesPerCluster, runLength * _bytesPerCluster)); } return extents.Count > 0; } return false; } /// 解析一条 MFT 记录的 $STANDARD_INFORMATION(0x10) / $FILE_NAME(0x30) / $DATA(0x80)。 internal static bool TryParseRecord(Span record, int bytesPerSector, out MftRecordInfo info) { info = default; info.Size = -1; info.NameLength = -1; if (record.Length < 56) return false; if (BinaryPrimitives.ReadUInt32LittleEndian(record) != FileRecordSignature) return false; if (!ApplyFixup(record, bytesPerSector)) return false; ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record[22..]); info.InUse = (flags & 0x0001) != 0; info.IsDirectory = (flags & 0x0002) != 0; int bestNamespace = -1; int offset = BinaryPrimitives.ReadUInt16LittleEndian(record[20..]); int guard = 0; while (offset >= 24 && offset + 8 <= record.Length && guard++ < 1024) { uint type = BinaryPrimitives.ReadUInt32LittleEndian(record[offset..]); if (type == AttrEnd) break; uint length = BinaryPrimitives.ReadUInt32LittleEndian(record[(offset + 4)..]); if (length < 24 || offset + length > record.Length) break; bool nonResident = record[offset + 8] != 0; if (!nonResident && type == AttrStandardInformation) { int valueOffset = BinaryPrimitives.ReadUInt16LittleEndian(record[(offset + 20)..]); int valueLength = (int)BinaryPrimitives.ReadUInt32LittleEndian(record[(offset + 16)..]); int v = offset + valueOffset; if (valueLength >= 36 && v + 36 <= record.Length) { info.CreatedFileTime = BinaryPrimitives.ReadInt64LittleEndian(record[v..]); info.ModifiedFileTime = BinaryPrimitives.ReadInt64LittleEndian(record[(v + 8)..]); info.Attributes = BinaryPrimitives.ReadUInt32LittleEndian(record[(v + 32)..]); } } else if (!nonResident && type == AttrFileName) { int valueOffset = BinaryPrimitives.ReadUInt16LittleEndian(record[(offset + 20)..]); int valueLength = (int)BinaryPrimitives.ReadUInt32LittleEndian(record[(offset + 16)..]); int v = offset + valueOffset; if (valueLength >= 66 && v + 66 <= record.Length) { int nameLength = record[v + 64]; int nameSpace = record[v + 65]; // 同一文件可能有多个 $FILE_NAME(硬链接 / 8.3 短名):优先 Win32 系列,跳过纯 DOS 名 bool better = bestNamespace < 0 || (bestNamespace == 2 && nameSpace != 2); if (better && v + 66 + nameLength * 2 <= record.Length) { bestNamespace = nameSpace; info.ParentRecordNumber = BinaryPrimitives.ReadUInt64LittleEndian(record[v..]) & UsnNative.RecordNumberMask; info.NameLength = nameLength; info.NameRecordOffset = v + 66; info.NameNamespace = (byte)nameSpace; } } } else if (type == AttrData) { if (nonResident) { if (offset + 56 <= record.Length) info.Size = BinaryPrimitives.ReadInt64LittleEndian(record[(offset + 48)..]); // RealSize } else { info.Size = BinaryPrimitives.ReadUInt32LittleEndian(record[(offset + 16)..]); // 驻留数据的大小 = 值长度 } } offset += (int)length; } if (info.IsDirectory) info.Size = -1; // 目录没有“文件大小”概念,统一记为未知 return true; } // ================================================================ 批量枚举 internal delegate void RecordVisitor(ulong recordNumber, Span record); /// /// 按区间流式读取整个 MFT,逐条回调(缓冲区复用,不产生 per-record 分配)。 /// 一般传 MftValidDataLength。 /// internal void Enumerate(RecordVisitor visitor, long maxByteOffset, Action? onBytesRead, CancellationToken cancellationToken) { var buffer = new byte[Math.Max(ReadBlockSize, _bytesPerRecord * 2)]; long consumed = 0; // 相对 MFT 起点的字节数 foreach (var (start, length) in _extents) { long extentRead = 0; while (extentRead + _bytesPerRecord <= length) { cancellationToken.ThrowIfCancellationRequested(); if (maxByteOffset > 0 && consumed >= maxByteOffset) return; long remaining = Math.Min(length - extentRead, maxByteOffset > 0 ? maxByteOffset - consumed : long.MaxValue); if (remaining < _bytesPerRecord) return; // 让每次读取边界都落在整条记录上,避免记录被拆到两次读里 int want = (int)Math.Min(buffer.Length, remaining); want -= want % _bytesPerRecord; if (want < _bytesPerRecord) want = _bytesPerRecord; int got = UsnNative.ReadAt(_volume, buffer.AsSpan(0, want), start + extentRead); if (got < _bytesPerRecord) return; got -= got % _bytesPerRecord; var span = buffer.AsSpan(0, got); for (int off = 0; off + _bytesPerRecord <= got; off += _bytesPerRecord) { ulong recordNumber = (ulong)((consumed + off) / _bytesPerRecord); visitor(recordNumber, span.Slice(off, _bytesPerRecord)); } extentRead += got; consumed += got; onBytesRead?.Invoke(consumed); if (got < want) return; } if (maxByteOffset > 0 && consumed >= maxByteOffset) return; } } /// /// 按记录号读一条 MFT 记录(走 run list 换算物理偏移)。用于增量监听时刷新单个文件的大小/时间。 /// internal bool TryReadRecord(ulong recordNumber, Span destination) { if (destination.Length < _bytesPerRecord) return false; long mftByteOffset = (long)recordNumber * _bytesPerRecord; foreach (var (start, length) in _extents) { if (mftByteOffset < length) { int got = UsnNative.ReadAt(_volume, destination[.._bytesPerRecord], start + mftByteOffset); return got == _bytesPerRecord; } mftByteOffset -= length; } return false; } }