Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)

This commit is contained in:
WpyQwq
2026-09-19 11:54:03 +08:00
commit 5780fde61a
60 changed files with 15035 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
using System.Buffers.Binary;
using Microsoft.Win32.SafeHandles;
namespace FluidExplorer.Services.Search.Usn;
/// <summary>从一条 $MFT 文件记录里解出来的关键字段。</summary>
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
}
/// <summary>
/// 原始 $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 索引。
/// </summary>
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;
/// <summary>MFT 在卷上的簇区间(已按字节换算),可用于日志与自检。</summary>
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;
}
/// <summary>
/// 尝试建立 MftReader。失败(非 NTFS、无权限、run list 解析不出来)返回 null 并通过 <paramref name="error"/> 说明原因。
/// </summary>
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
/// <summary>
/// 应用 NTFS 的 fixup(Update Sequence Array)修正:
/// 每个扇区最后 2 个字节在磁盘上被换成了 USA 里的“更新序列号”,
/// 必须在解析前用 USA 中保存的真实值逐个还原,否则跨扇区的字段会是垃圾数据。
/// 返回 false 表示 USN 不匹配(记录在读取过程中被改写或已损坏),调用方应跳过该记录。
/// 设计成 static 是为了能被“合成记录”单元测试直接调用(无需真实卷句柄)。
/// </summary>
internal static bool ApplyFixup(Span<byte> 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;
}
// ================================================================ 属性解析
/// <summary>
/// 从记录里读取 $DATA(0x80) 的 mapping pairs(run list),换算成卷内字节区间。
/// 这里刻意手写属性链循环而不用委托回调:该路径在构建期会被调用百万次,闭包分配不可接受。
/// </summary>
private bool TryReadDataRuns(ReadOnlySpan<byte> 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;
}
/// <summary>解析一条 MFT 记录的 $STANDARD_INFORMATION(0x10) / $FILE_NAME(0x30) / $DATA(0x80)。</summary>
internal static bool TryParseRecord(Span<byte> 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<byte> record);
/// <summary>
/// 按区间流式读取整个 MFT,逐条回调(缓冲区复用,不产生 per-record 分配)。
/// <paramref name="maxByteOffset"/> 一般传 MftValidDataLength。
/// </summary>
internal void Enumerate(RecordVisitor visitor, long maxByteOffset, Action<long>? 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;
}
}
/// <summary>
/// 按记录号读一条 MFT 记录(走 run list 换算物理偏移)。用于增量监听时刷新单个文件的大小/时间。
/// </summary>
internal bool TryReadRecord(ulong recordNumber, Span<byte> 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;
}
}