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
@@ -0,0 +1,243 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace FluidExplorer.Services.Search.Usn;
/// <summary>
/// 增量监听的<b>回退通道</b>:ReadDirectoryChangesW。
///
/// 什么情况下用:USN 变更日志不可用(卷上没日志且没权限创建、被策略禁用、日志刚被删除等)。
/// 这条路径不需要任何特殊权限,普通用户也能跑,从而保证「索引实时」这个卖点不落空。
///
/// 与 USN 的差别与应对:
/// * RDCW 只给相对<b>路径</b>,不给 FRN。这里用 CreateFileW(FILE_READ_ATTRIBUTES) +
/// GetFileInformationByHandle 取句柄上的 FileIndex —— 它与 USN 的 FRN 完全同口径
/// (低 48 位记录号 + 高 16 位序列号),因此能直接命中同一个索引条目。
/// * 新增/改名/内容变化都伴随着文件仍然存在,可以 stat 出来 → 直接 upsert。
/// * 删除只剩一个路径(stat 必然失败),无法反查 FRN。应对:把受影响的<b>目录</b>记下来,
/// 批处理结束后对该目录做一次「磁盘现状 vs 索引」的对账(<see cref="UsnVolumeIndex.ResyncDirectoryFromDisk"/>),
/// 只把索引里存在、磁盘上已消失的孩子打墓碑。对账按目录去重、每轮有上限,代价可控。
/// </summary>
internal sealed class DirectoryChangeWatcher
{
private const int ReadBufferSize = 64 * 1024;
/// <summary>每轮最多对账多少个目录,防止一次批量删除把 CPU 打满。</summary>
private const int MaxResyncDirectoriesPerPass = 256;
private static readonly uint NotifyFilter =
UsnNative.FILE_NOTIFY_CHANGE_FILE_NAME |
UsnNative.FILE_NOTIFY_CHANGE_DIR_NAME |
UsnNative.FILE_NOTIFY_CHANGE_SIZE |
UsnNative.FILE_NOTIFY_CHANGE_LAST_WRITE |
UsnNative.FILE_NOTIFY_CHANGE_ATTRIBUTES;
private readonly UsnVolumeIndex _index;
private readonly string _root;
private readonly ConcurrentDictionary<string, ulong> _directoryFrnCache = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _pendingResync = new(StringComparer.OrdinalIgnoreCase);
private readonly List<string> _resyncScratch = [];
private SafeFileHandle? _handle;
private volatile bool _stopRequested;
internal DirectoryChangeWatcher(UsnVolumeIndex index)
{
_index = index;
_root = index.VolumeRoot;
}
/// <summary>打断阻塞中的 ReadDirectoryChangesW:置停止标志 + 关目录句柄(双保险)。</summary>
internal void RequestStop()
{
_stopRequested = true;
Interlocked.Exchange(ref _handle, null)?.Dispose();
}
/// <summary>打开卷根目录句柄。返回 false 时 <paramref name="error"/> 给出中文原因。</summary>
internal bool TryOpen(out string? error)
{
error = null;
var handle = UsnNative.CreateFileW(
_root,
UsnNative.FILE_LIST_DIRECTORY,
UsnNative.FILE_SHARE_READ | UsnNative.FILE_SHARE_WRITE | UsnNative.FILE_SHARE_DELETE,
IntPtr.Zero,
UsnNative.OPEN_EXISTING,
UsnNative.FILE_FLAG_BACKUP_SEMANTICS, // 打开目录必须带这个
IntPtr.Zero);
if (handle.IsInvalid)
{
int openError = Marshal.GetLastWin32Error();
handle.Dispose();
error = UsnNative.DescribeError(openError, $"无法以 FILE_LIST_DIRECTORY 打开 {_root}");
return false;
}
Volatile.Write(ref _handle, handle);
return true;
}
/// <summary>阻塞式监听循环,直到 <see cref="RequestStop"/> 或句柄被关闭。</summary>
internal void Run()
{
var handle = Volatile.Read(ref _handle);
if (handle is null) return;
var buffer = new byte[ReadBufferSize];
try
{
while (!_stopRequested)
{
uint returned;
bool ok;
unsafe
{
fixed (byte* p = buffer)
{
ok = UsnNative.ReadDirectoryChangesW(
handle, p, (uint)buffer.Length, true, NotifyFilter, out returned, IntPtr.Zero, IntPtr.Zero);
}
}
if (!ok)
{
int readError = Marshal.GetLastWin32Error();
if (_stopRequested
|| readError is UsnNative.ERROR_OPERATION_ABORTED or UsnNative.ERROR_CANCELLED or UsnNative.ERROR_INVALID_HANDLE)
{
break;
}
// 单次失败不放弃:报给 UI 后继续(例如枚举期间目录被临时独占)
_index.ReportWatchIssue($"ReadDirectoryChangesW 出错:{UsnNative.DescribeError(readError)}");
continue;
}
if (returned == 0)
{
// 变更缓冲区溢出:期间的事件已经丢了,只能提示 UI 做一次重建
_index.ReportWatchIssue("变更缓冲区溢出,部分变更已丢失,建议重建索引以保持一致。");
continue;
}
ProcessEvents(buffer.AsSpan(0, (int)returned));
FlushPendingResync();
}
}
finally
{
Interlocked.Exchange(ref _handle, null)?.Dispose();
}
}
private void ProcessEvents(ReadOnlySpan<byte> buffer)
{
int offset = 0;
while (true)
{
if (offset + UsnNative.FileNotifyInformation.HeaderSize > buffer.Length) break;
uint nextEntry = BinaryPrimitives.ReadUInt32LittleEndian(buffer[offset..]);
uint action = BinaryPrimitives.ReadUInt32LittleEndian(buffer[(offset + 4)..]);
int nameBytes = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer[(offset + 8)..]);
int nameOffset = offset + UsnNative.FileNotifyInformation.HeaderSize;
if (nameBytes <= 0 || nameOffset + nameBytes > buffer.Length) break;
// 文件名是相对被监听目录(这里是卷根)的路径,形如 "Users\Public\x.txt",不以 NUL 结尾
var relative = MemoryMarshal.Cast<byte, char>(buffer.Slice(nameOffset, nameBytes));
Handle(relative, action);
if (nextEntry == 0) break;
offset += (int)nextEntry;
}
}
private void Handle(ReadOnlySpan<char> relative, uint action)
{
if (relative.Length == 0) return;
var fullPath = string.Concat(_root, relative);
switch (action)
{
case UsnNative.FILE_ACTION_ADDED:
case UsnNative.FILE_ACTION_RENAMED_NEW_NAME:
case UsnNative.FILE_ACTION_MODIFIED:
UpsertFromDisk(fullPath);
break;
case UsnNative.FILE_ACTION_REMOVED:
case UsnNative.FILE_ACTION_RENAMED_OLD_NAME:
QueueDirectoryResync(fullPath);
break;
}
}
/// <summary>按路径 stat 出 FRN + 元数据,然后按 FRN 落入索引(存在则更新,不存在则新增)。</summary>
private void UpsertFromDisk(string fullPath)
{
if (!UsnNative.TryStatPath(fullPath, out var info)) return; // 文件已再次消失等,忽略
var name = Path.GetFileName(fullPath.AsSpan());
if (name.Length == 0) return;
ulong parentFrn = 0;
var parentPath = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(parentPath) && !parentPath.Equals(_root, StringComparison.OrdinalIgnoreCase))
{
parentFrn = GetDirectoryFrn(parentPath);
}
bool isDirectory = (info.FileAttributes & UsnNative.FILE_ATTRIBUTE_DIRECTORY) != 0;
_index.UpsertFromFileSystem(
info.FileIndex,
parentFrn,
name,
isDirectory,
isDirectory ? 0 : info.FileSize,
UsnVolumeIndex.FileTimeToTicks(info.LastWriteTime.ToInt64()),
info.FileAttributes);
if (isDirectory) _directoryFrnCache[fullPath] = UsnNative.NormalizeFrn(info.FileIndex);
}
private ulong GetDirectoryFrn(string directoryPath)
{
if (_directoryFrnCache.TryGetValue(directoryPath, out var cached)) return cached;
if (!UsnNative.TryStatPath(directoryPath, out var info)) return 0;
var frn = UsnNative.NormalizeFrn(info.FileIndex);
_directoryFrnCache[directoryPath] = frn;
return frn;
}
private void QueueDirectoryResync(string fullPath)
{
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory)) _pendingResync.Add(directory);
}
private void FlushPendingResync()
{
if (_pendingResync.Count == 0) return;
_resyncScratch.Clear();
foreach (var directory in _pendingResync)
{
_resyncScratch.Add(directory);
if (_resyncScratch.Count >= MaxResyncDirectoriesPerPass) break;
}
int removed = 0;
foreach (var directory in _resyncScratch)
{
_pendingResync.Remove(directory);
removed += _index.ResyncDirectoryFromDisk(directory);
}
if (removed > 0) _index.ReportWatchIssue(null, removed);
}
}
+212
View File
@@ -0,0 +1,212 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace FluidExplorer.Services.Search.Usn;
/// <summary>
/// 索引的紧凑存储:结构体数组(Struct-of-Arrays)而不是对象数组。
///
/// 每条记录只占 45 字节(8+8+8+8+8+4+1),100 万条约 45MB,
/// 再加上名字池(约 2 字节/字符)就构成整个索引;绝无 per-entry 的对象头与 string。
///
/// 并发模型(读多写极少):
/// * <see cref="Count"/> 用 volatile 发布:写入方先写满数据,最后自增 Count;读方只需读一次 Count 再顺序访问。
/// * 结构只增不减(删除只打墓碑标志位),因此已经发布的 [0, Count) 区间永远有效。
/// * 容量不足时 <see cref="Grow"/> 采用 copy-on-write,整体替换数组;老快照对正在查询的线程依然合法。
/// </summary>
internal sealed class IndexStore
{
internal const byte FlagDeleted = 0x01;
internal const byte FlagDirectory = 0x02;
internal const byte FlagSizeKnown = 0x04;
internal readonly ulong[] Frn; // 归一化 FRN(低 48 位记录号)
internal readonly ulong[] ParentFrn; // 归一化父目录 FRN
internal readonly long[] NameRef; // NamePool.Pack(偏移, 长度)
internal readonly long[] Size; // -1 = 未知(USN 降级模式)
internal readonly long[] ModifiedTicks; // UTC ticks;0 = 未知
internal readonly uint[] Attributes; // FILE_ATTRIBUTE_*
internal readonly byte[] Flags;
/// <summary>
/// 名字池与数组快照绑定在一起:查询只需抓取一次 store 引用就能得到一致的 (名字池, 数组, 条数)。
/// 关键:扩容(<see cref="Grow"/>)必须<b>复用同一个名字池</b> —— NameRef 里存的是池内偏移,
/// 换池等于让所有老记录的名字全部失效。
/// </summary>
internal readonly NamePool Names;
internal readonly int Capacity;
/// <summary>已发布条数。写入方必须在写完所有字段之后再自增它。</summary>
internal volatile int Count;
internal IndexStore(int capacity) : this(capacity, new NamePool())
{
}
private IndexStore(int capacity, NamePool names)
{
if (capacity < 16) capacity = 16;
Capacity = capacity;
Names = names;
Frn = new ulong[capacity];
ParentFrn = new ulong[capacity];
NameRef = new long[capacity];
Size = new long[capacity];
ModifiedTicks = new long[capacity];
Attributes = new uint[capacity];
Flags = new byte[capacity];
}
private IndexStore(IndexStore old, int capacity) : this(capacity, old.Names)
{
int n = old.Count;
Array.Copy(old.Frn, Frn, n);
Array.Copy(old.ParentFrn, ParentFrn, n);
Array.Copy(old.NameRef, NameRef, n);
Array.Copy(old.Size, Size, n);
Array.Copy(old.ModifiedTicks, ModifiedTicks, n);
Array.Copy(old.Attributes, Attributes, n);
Array.Copy(old.Flags, Flags, n);
Count = n;
}
/// <summary>扩容(copy-on-write)。返回新快照,旧快照仍然可被并发查询安全使用。</summary>
internal static IndexStore Grow(IndexStore old, int minCapacity)
{
int capacity = old.Capacity;
while (capacity < minCapacity) capacity = capacity < 1024 ? capacity * 2 : capacity + (capacity >> 1);
return new IndexStore(old, capacity);
}
internal bool IsDeleted(int i) => (Flags[i] & FlagDeleted) != 0;
internal bool IsDirectory(int i) => (Flags[i] & FlagDirectory) != 0;
/// <summary>读名字(零分配)。</summary>
internal ReadOnlySpan<char> GetName(int i)
{
long nameRef = Volatile.Read(ref NameRef[i]);
return Names.Get(NamePool.UnpackOffset(nameRef), NamePool.UnpackLength(nameRef));
}
}
/// <summary>
/// FRN(低 48 位记录号)→ 索引下标的映射。
///
/// 策略:构建期用普通 Dictionary 最快;构建结束后调用 <see cref="Optimize"/>,
/// 如果记录号足够密集(maxRecord &lt;= 8 * count),就换成“记录号直接寻址”的 int[],
/// 100 万文件只占几 MB(而 Dictionary 要 30~40MB)。
/// 稀疏卷或监听期新增的越界记录号退回到 <see cref="ConcurrentDictionary{TKey,TValue}"/> 侧表。
/// </summary>
internal sealed class FrnMap
{
private const int NoIndex = 0;
private const int DenseSlack = 8;
private Dictionary<ulong, int>? _buildMap;
private int[]? _dense; // 记录号 → 下标+1;0 表示不存在
private ConcurrentDictionary<ulong, int>? _sparse;
internal FrnMap(int estimatedCount)
{
_buildMap = new Dictionary<ulong, int>(Math.Max(16, estimatedCount));
}
internal int Count => _buildMap?.Count ?? _sparse?.Count ?? _dense?.Length ?? 0;
/// <summary>构建期写入(单线程,无锁,最快)。</summary>
internal void AddBuild(ulong frn, int index)
{
_buildMap![UsnNative.NormalizeFrn(frn)] = index;
}
/// <summary>构建完成后调用:把构建期的 Dictionary 压缩成密集数组或稀疏侧表。</summary>
internal void Optimize()
{
var map = _buildMap ?? throw new InvalidOperationException("FrnMap 已经压缩过。");
_buildMap = null;
ulong maxRecord = 0;
foreach (var key in map.Keys)
{
if (key > maxRecord) maxRecord = key;
}
// 密集数组代价 = (maxRecord+1)*4 字节;只要不超过 8 个记录号/条目的开销就值得。
if (map.Count > 0 && maxRecord <= (ulong)map.Count * DenseSlack)
{
var dense = new int[maxRecord + 1];
foreach (var (key, value) in map)
{
dense[key] = value + 1;
}
Volatile.Write(ref _dense, dense);
return;
}
var sparse = new ConcurrentDictionary<ulong, int>();
foreach (var (key, value) in map)
{
sparse[key] = value;
}
Volatile.Write(ref _sparse, sparse);
}
/// <summary>监听期写入(可能并发于查询,必须线程安全)。</summary>
internal void Set(ulong frn, int index)
{
ulong record = UsnNative.NormalizeFrn(frn);
var dense = Volatile.Read(ref _dense);
if (dense is not null && record < (ulong)dense.Length)
{
Volatile.Write(ref dense[record], index + 1);
return;
}
var sparse = _sparse;
if (sparse is null)
{
var created = new ConcurrentDictionary<ulong, int>();
sparse = Interlocked.CompareExchange(ref _sparse, created, null) ?? created;
}
sparse[record] = index;
}
/// <summary>批量覆盖(重建索引时用,单线程)。</summary>
internal void SetUnsafe(ulong frn, int index)
{
ulong record = UsnNative.NormalizeFrn(frn);
var dense = _dense;
if (dense is not null && record < (ulong)dense.Length)
{
dense[record] = index + 1;
return;
}
_sparse![record] = index;
}
internal bool TryGet(ulong frn, out int index)
{
ulong record = UsnNative.NormalizeFrn(frn);
var dense = Volatile.Read(ref _dense);
if (dense is not null && record < (ulong)dense.Length)
{
int slot = Volatile.Read(ref dense[record]);
if (slot == NoIndex)
{
index = -1;
return false;
}
index = slot - 1;
return true;
}
var sparse = Volatile.Read(ref _sparse);
if (sparse is not null && sparse.TryGetValue(record, out index)) return true;
index = -1;
return false;
}
/// <summary>NormizeFrn 的别名,便于调用点表达意图。</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong Normalize(ulong frn) => UsnNative.NormalizeFrn(frn);
}
+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;
}
}
+83
View File
@@ -0,0 +1,83 @@
namespace FluidExplorer.Services.Search.Usn;
/// <summary>
/// 紧凑文件名池 —— 索引“快且省内存”的关键之一。
///
/// 所有文件名以 UTF-16 连续存放在若干 <b>定长块</b>(每块 1&lt;&lt;20 个字符 = 2MB)里,
/// 整个索引里不会为文件名产生任何 string 对象;查询时直接在这块内存上取 ReadOnlySpan&lt;char&gt;。
///
/// 偏移编码:<c>(chunkIndex &lt;&lt; 20) | offsetInChunk</c>,单个 int 即可寻址 2G 字符。
/// 因为块大小固定为 1&lt;&lt;20,而 NTFS 单个文件名最长 255 字符,
/// 所以一个名字永远不会跨越两个块 —— 取值时无需拼接。
///
/// 线程安全:块数组一次性预分配(只写指针不扩容),块本身写入后用 <see cref="Volatile.Write{T}(ref T, T)"/> 发布。
/// 查询线程只会读到“已经完整写入”的块;配合索引条数的发布顺序(先写数据、后写 Count),读侧无需加锁。
/// </summary>
internal sealed class NamePool
{
internal const int ChunkShift = 20;
internal const int ChunkSize = 1 << ChunkShift;
internal const int ChunkMask = ChunkSize - 1;
/// <summary>NameRef 只用 8 位存长度,NTFS 文件名最长 255 字符,正好用满。</summary>
internal const int MaxNameLength = 255;
private const int MaxChunks = 2048; // 2048 * 1M = 2G 字符上限
private readonly char[][] _chunks = new char[MaxChunks][];
private int _chunkCount;
private int _fill; // 当前块已使用字符数
private int _publishedChars;
internal NamePool()
{
AddChunk();
}
/// <summary>池中已发布的字符总数(约等于所有名字长度之和)。</summary>
internal int TotalChars => _publishedChars;
/// <summary>写入一个名字,返回它的池内偏移。空名字返回 0(配合 NameRef 的长度字段仍然无歧义)。</summary>
internal int Add(ReadOnlySpan<char> name)
{
if (name.Length == 0) return 0;
if (name.Length > ChunkSize) name = name[..ChunkSize];
if (_fill + name.Length > ChunkSize) AddChunk();
int chunkIndex = _chunkCount - 1;
int offset = (chunkIndex << ChunkShift) | _fill;
name.CopyTo(_chunks[chunkIndex].AsSpan(_fill));
_fill += name.Length;
_publishedChars = (chunkIndex << ChunkShift) + _fill;
return offset;
}
/// <summary>取回名字。参数非法时返回空 span(绝不抛异常,读侧可能并发读到正在更新的旧值)。</summary>
internal ReadOnlySpan<char> Get(int offset, int length)
{
if (length <= 0 || offset < 0) return default;
int chunkIndex = offset >> ChunkShift;
int inChunk = offset & ChunkMask;
if (chunkIndex >= _chunkCount) return default;
var chunk = Volatile.Read(ref _chunks[chunkIndex]);
if (chunk is null || inChunk + length > ChunkSize) return default;
return chunk.AsSpan(inChunk, length);
}
private void AddChunk()
{
if (_chunkCount >= MaxChunks) throw new InvalidOperationException("文件名池已达到容量上限(2G 字符)。");
var chunk = new char[ChunkSize];
Volatile.Write(ref _chunks[_chunkCount], chunk);
_chunkCount++;
_fill = 0;
}
// ---------------------------------------------------------------- NameRef 打包
/// <summary>把 (偏移, 长度) 打包进一个 long:低 8 位是长度,高 56 位是偏移。</summary>
internal static long Pack(int offset, int length) => ((long)offset << 8) | (uint)(length & 0xFF);
internal static int UnpackOffset(long nameRef) => (int)(nameRef >> 8);
internal static int UnpackLength(long nameRef) => (int)(nameRef & 0xFF);
}
+458
View File
@@ -0,0 +1,458 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace FluidExplorer.Services.Search.Usn;
/// <summary>
/// USN 日志 / NTFS 卷所需的最小 P/Invoke 与结构体集合。
///
/// 布局约定:所有 struct 均为 <see cref="LayoutKind.Sequential"/>,默认采用 x64 自然对齐;
/// 字段顺序与偏移和 Windows SDK 的 winioctl.h / ntifs.h 完全一致。
/// 每个结构体后面都标注了实测字节大小,便于对照 <see cref="Marshal.SizeOf{T}()"/> 校验。
/// </summary>
internal static class UsnNative
{
// ---------------------------------------------------------------- 访问权限 / 打开方式
internal const uint GENERIC_READ = 0x80000000;
internal const uint GENERIC_WRITE = 0x40000000;
internal const uint FILE_READ_DATA = 0x0001;
internal const uint FILE_READ_ATTRIBUTES = 0x0080;
internal const uint FILE_LIST_DIRECTORY = 0x0001;
internal const uint FILE_SHARE_READ = 0x00000001;
internal const uint FILE_SHARE_WRITE = 0x00000002;
internal const uint FILE_SHARE_DELETE = 0x00000004;
internal const uint OPEN_EXISTING = 3;
/// <summary>
/// 打开 <c>\\.\C:</c> 时用的 dwFlagsAndAttributes。
/// 参考实现(Everything)用 FILE_ATTRIBUTE_READONLY:传 FILE_ATTRIBUTE_NORMAL 在部分环境下会开不了卷句柄。
/// </summary>
internal const uint FILE_ATTRIBUTE_READONLY = 0x00000001;
/// <summary>开目录句柄必须带 FILE_FLAG_BACKUP_SEMANTICS,否则 CreateFile 会失败。</summary>
internal const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
/// <summary>顺序扫描提示:读 MFT 时对缓存友好(读一次不再复用)。</summary>
internal const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
// ---------------------------------------------------------------- 控制码 CTL_CODE(FILE_DEVICE_FILE_SYSTEM=0x09, ...)
internal const uint FSCTL_ENUM_USN_DATA = 0x000900B3;
internal const uint FSCTL_READ_USN_JOURNAL = 0x000900BB;
internal const uint FSCTL_QUERY_USN_JOURNAL = 0x000900F4;
internal const uint FSCTL_CREATE_USN_JOURNAL = 0x000900E7;
internal const uint FSCTL_DELETE_USN_JOURNAL = 0x000900F8;
internal const uint FSCTL_GET_NTFS_VOLUME_DATA = 0x00090064;
internal const uint FSCTL_GET_NTFS_FILE_RECORD = 0x00090068;
/// <summary>FSCTL_DELETE_USN_JOURNAL 的 DeleteFlags:真正删除日志。</summary>
internal const uint USN_DELETE_FLAG_DELETE = 0x00000001;
internal const uint USN_DELETE_FLAG_DO_NOT_DELETE = 0x00000000;
// ---------------------------------------------------------------- ReadDirectoryChangesW(USN 日志不可用时的回退监听)
internal const uint FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001;
internal const uint FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002;
internal const uint FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004;
internal const uint FILE_NOTIFY_CHANGE_SIZE = 0x00000008;
internal const uint FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010;
internal const uint FILE_ACTION_ADDED = 0x00000001;
internal const uint FILE_ACTION_REMOVED = 0x00000002;
internal const uint FILE_ACTION_MODIFIED = 0x00000003;
internal const uint FILE_ACTION_RENAMED_OLD_NAME = 0x00000004;
internal const uint FILE_ACTION_RENAMED_NEW_NAME = 0x00000005;
// ---------------------------------------------------------------- Win32 错误码
internal const int ERROR_INVALID_FUNCTION = 1;
internal const int ERROR_ACCESS_DENIED = 5;
internal const int ERROR_INVALID_HANDLE = 6;
internal const int ERROR_NOT_READY = 21;
internal const int ERROR_HANDLE_EOF = 38;
internal const int ERROR_NOT_SUPPORTED = 50;
internal const int ERROR_INVALID_PARAMETER = 87;
internal const int ERROR_MORE_DATA = 234;
internal const int ERROR_OPERATION_ABORTED = 995;
internal const int ERROR_NOTIFY_ENUM_DIR = 1022;
internal const int ERROR_JOURNAL_DELETE_IN_PROGRESS = 1178;
internal const int ERROR_JOURNAL_NOT_ACTIVE = 1179;
internal const int ERROR_JOURNAL_ENTRY_DELETED = 1181;
internal const int ERROR_CANCELLED = 1223;
// ---------------------------------------------------------------- 文件属性
internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
// ---------------------------------------------------------------- USN 变更原因
internal const uint USN_REASON_DATA_OVERWRITE = 0x00000001;
internal const uint USN_REASON_DATA_EXTEND = 0x00000002;
internal const uint USN_REASON_DATA_TRUNCATION = 0x00000004;
internal const uint USN_REASON_NAMED_DATA_OVERWRITE = 0x00000010;
internal const uint USN_REASON_NAMED_DATA_EXTEND = 0x00000020;
internal const uint USN_REASON_NAMED_DATA_TRUNCATION = 0x00000040;
internal const uint USN_REASON_FILE_CREATE = 0x00000100;
internal const uint USN_REASON_FILE_DELETE = 0x00000200;
internal const uint USN_REASON_EA_CHANGE = 0x00000400;
internal const uint USN_REASON_SECURITY_CHANGE = 0x00000800;
internal const uint USN_REASON_RENAME_OLD_NAME = 0x00001000;
internal const uint USN_REASON_RENAME_NEW_NAME = 0x00002000;
internal const uint USN_REASON_INDEXABLE_CHANGE = 0x00004000;
internal const uint USN_REASON_BASIC_INFO_CHANGE = 0x00008000;
internal const uint USN_REASON_HARD_LINK_CHANGE = 0x00010000;
internal const uint USN_REASON_COMPRESSION_CHANGE = 0x00020000;
internal const uint USN_REASON_ENCRYPTION_CHANGE = 0x00040000;
internal const uint USN_REASON_OBJECT_ID_CHANGE = 0x00080000;
internal const uint USN_REASON_REPARSE_POINT_CHANGE = 0x00100000;
internal const uint USN_REASON_STREAM_CHANGE = 0x00200000;
internal const uint USN_REASON_CLOSE = 0x80000000;
internal const uint USN_REASON_ANY = 0xFFFFFFFF;
// ---------------------------------------------------------------- 线程访问权限(CancelSynchronousIo 需要 THREAD_TERMINATE)
internal const uint THREAD_TERMINATE = 0x0001;
internal static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
// ================================================================ 结构体
/// <summary>MFT_ENUM_DATA_V0 —— FSCTL_ENUM_USN_DATA 的输入。x64 大小 24。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct MftEnumDataV0
{
internal ulong StartFileReferenceNumber; // 0
internal long LowUsn; // 8 枚举时用 0
internal long HighUsn; // 16 枚举时用 long.MaxValue
}
/// <summary>USN_JOURNAL_DATA_V0 —— FSCTL_QUERY_USN_JOURNAL 的输出。x64 大小 56。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct UsnJournalDataV0
{
internal ulong UsnJournalID; // 0
internal long FirstUsn; // 8
internal long NextUsn; // 16
internal long LowestValidUsn; // 24
internal long MaxUsn; // 32
internal ulong MaximumSize; // 40
internal ulong AllocationDelta; // 48
}
/// <summary>READ_USN_JOURNAL_DATA_V0 —— FSCTL_READ_USN_JOURNAL 的输入。x64 大小 40。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ReadUsnJournalDataV0
{
internal long StartUsn; // 0
internal uint ReasonMask; // 8
internal uint ReturnOnlyOnClose; // 12
internal ulong Timeout; // 16 100ns 单位;0 = 无限等待
internal ulong BytesToWaitFor; // 24 攒够这么多字节再返回(减少唤醒次数)
internal ulong UsnJournalID; // 32
}
/// <summary>
/// USN_RECORD_V2 的固定头部(不含变长文件名)。
///
/// 字段偏移与 Windows SDK 完全一致(x64):RecordLength@0、MajorVersion@4、FRN@8、
/// ParentFrn@16、Usn@24、TimeStamp@32、Reason@40、SourceInfo@44、SecurityId@48、
/// FileAttributes@52、FileNameLength@56、FileNameOffset@58;变长文件名紧跟在第 60 字节之后。
///
/// 显式 Pack=4 的原因:默认 8 字节对齐会把 sizeof 从 60 凑成 64(尾部补 4 字节),
/// 字段偏移虽然不变,但用 sizeof(T) 做缓冲边界判断会凭空多要求 4 字节;
/// Pack=4 下偏移完全不变、sizeof 恰好 60,与磁盘上的紧凑布局严格一致。
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct UsnRecordV2
{
internal uint RecordLength; // 0
internal ushort MajorVersion; // 4 必须 == 2
internal ushort MinorVersion; // 6
internal ulong FileReferenceNumber; // 8 高 16 位是序列号,低 48 位是 MFT 记录号
internal ulong ParentFileReferenceNumber; // 16 同上
internal long Usn; // 24
internal long TimeStamp; // 32 FILETIME(100ns since 1601)
internal uint Reason; // 40
internal uint SourceInfo; // 44
internal uint SecurityId; // 48
internal uint FileAttributes; // 52
internal ushort FileNameLength; // 56 字节数,非字符数
internal ushort FileNameOffset; // 58 相对记录起始的字节偏移
internal const int Size = 60;
}
/// <summary>NTFS_VOLUME_DATA_BUFFER —— FSCTL_GET_NTFS_VOLUME_DATA 的输出。x64 大小 96。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct NtfsVolumeDataBuffer
{
internal long VolumeSerialNumber; // 0
internal long NumberSectors; // 8
internal long TotalClusters; // 16
internal long FreeClusters; // 24
internal long TotalReserved; // 32
internal uint BytesPerSector; // 40
internal uint BytesPerCluster; // 44
internal uint BytesPerFileRecordSegment; // 48
internal uint ClustersPerFileRecordSegment; // 52
internal long MftValidDataLength; // 56
internal long MftStartLcn; // 64
internal long Mft2StartLcn; // 72
internal long MftZoneStart; // 80
internal long MftZoneEnd; // 88
}
/// <summary>NTFS_FILE_RECORD_INPUT_BUFFER —— FSCTL_GET_NTFS_FILE_RECORD 的输入。x64 大小 8。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct NtfsFileRecordInputBuffer
{
internal ulong FileReferenceNumber; // 只取低 48 位记录号
}
/// <summary>NTFS_FILE_RECORD_OUTPUT_BUFFER 的固定头。x64 大小 12(后面紧跟变长记录)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct NtfsFileRecordOutputBuffer
{
internal ulong FileReferenceNumber; // 0
internal uint FileRecordLength; // 8
}
/// <summary>FSCTL_CREATE_USN_JOURNAL 的输入。x64 大小 16;两个字段都为 0 = 使用系统默认值。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct CreateUsnJournalData
{
internal ulong MaximumSize; // 0 = 系统默认
internal ulong AllocationDelta; // 0 = 系统默认
}
/// <summary>FSCTL_DELETE_USN_JOURNAL 的输入。x64 大小 16(ulong + DWORD + 对齐填充)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct DeleteUsnJournalData
{
internal ulong UsnJournalID;
internal uint DeleteFlags;
}
/// <summary>
/// FILETIME 的精确布局:两个 DWORD。
/// 刻意不用 long —— 在 LayoutKind.Sequential 下 long 会带来 8 字节对齐,
/// 从而把 BY_HANDLE_FILE_INFORMATION 的后续字段全部顶偏。
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct FileTimeValue
{
internal uint LowDateTime;
internal uint HighDateTime;
internal readonly long ToInt64() => ((long)HighDateTime << 32) | LowDateTime;
}
/// <summary>
/// BY_HANDLE_FILE_INFORMATION(GetFileInformationByHandle 的输出)。x64 大小 52。
/// 其中 FileIndexHigh/Low 合起来就是文件的 64 位 FRN(低 48 位记录号 + 高 16 位序列号),
/// 与 USN_RECORD_V2 里的 FileReferenceNumber 口径一致 —— 这是 RDCW 回退路径能定位索引条目的关键。
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ByHandleFileInformation
{
internal uint FileAttributes; // 0
internal FileTimeValue CreationTime; // 4
internal FileTimeValue LastAccessTime; // 12
internal FileTimeValue LastWriteTime; // 20
internal uint VolumeSerialNumber; // 28
internal uint FileSizeHigh; // 32
internal uint FileSizeLow; // 36
internal uint NumberOfLinks; // 40
internal uint FileIndexHigh; // 44
internal uint FileIndexLow; // 48
internal readonly ulong FileIndex => ((ulong)FileIndexHigh << 32) | FileIndexLow;
internal readonly long FileSize => ((long)FileSizeHigh << 32) | FileSizeLow;
}
/// <summary>FILE_NOTIFY_INFORMATION 的固定头(变长文件名紧跟其后,UTF-16,不以 NUL 结尾)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct FileNotifyInformation
{
internal uint NextEntryOffset; // 0
internal uint Action; // 4
internal uint FileNameLength; // 8,字节数
// WCHAR FileName[1]; // 12
internal const int HeaderSize = 12;
}
// ================================================================ P/Invoke
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern SafeFileHandle CreateFileW(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern unsafe bool DeviceIoControl(
SafeFileHandle hDevice,
uint dwIoControlCode,
void* lpInBuffer,
uint nInBufferSize,
void* lpOutBuffer,
uint nOutBufferSize,
out uint lpBytesReturned,
IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern unsafe bool ReadFile(
SafeFileHandle hFile,
void* lpBuffer,
uint nNumberOfBytesToRead,
out uint lpNumberOfBytesRead,
IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern unsafe bool SetFilePointerEx(
SafeFileHandle hFile,
long liDistanceToMove,
out long lpNewFilePointer,
uint dwMoveMethod);
internal const uint FILE_BEGIN = 0;
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr OpenThread(uint dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern uint GetCurrentThreadId();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CancelSynchronousIo(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool GetFileInformationByHandle(SafeFileHandle hFile, out ByHandleFileInformation lpFileInformation);
/// <summary>
/// 递归监听目录变化。lpOverlapped == NULL 时是同步阻塞调用(靠 CancelSynchronousIo 打断)。
/// 返回 TRUE 且 bytesReturned == 0 表示变更缓冲区溢出(ERROR_NOTIFY_ENUM_DIR),期间的事件已丢失。
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern unsafe bool ReadDirectoryChangesW(
SafeFileHandle hDirectory,
void* lpBuffer,
uint nBufferLength,
bool bWatchSubtree,
uint dwNotifyFilter,
out uint lpBytesReturned,
IntPtr lpOverlapped,
IntPtr lpCompletionRoutine);
// ================================================================ 托管包装
/// <summary>把读/写缓冲固定后调用 DeviceIoControl;返回 false 时用 <see cref="Marshal.GetLastWin32Error"/> 取错误码。</summary>
internal static unsafe bool Ioctl(SafeFileHandle handle, uint code, ReadOnlySpan<byte> input, Span<byte> output, out int bytesReturned)
{
fixed (byte* pIn = input)
fixed (byte* pOut = output)
{
var ok = DeviceIoControl(
handle, code,
input.Length == 0 ? null : pIn, (uint)input.Length,
output.Length == 0 ? null : pOut, (uint)output.Length,
out var ret, IntPtr.Zero);
bytesReturned = (int)ret;
return ok;
}
}
/// <summary>在卷句柄上做一次带偏移的同步读取(卷句柄偏移 = 卷内绝对字节偏移)。</summary>
internal static unsafe int ReadAt(SafeFileHandle handle, Span<byte> buffer, long offset)
{
if (!SetFilePointerEx(handle, offset, out _, FILE_BEGIN)) return -1;
fixed (byte* p = buffer)
{
if (!ReadFile(handle, p, (uint)buffer.Length, out var read, IntPtr.Zero)) return -1;
return (int)read;
}
}
/// <summary>
/// 按路径取文件的 64 位 FRN 与基本元数据(大小/属性/最后写入时间)。
/// 这是 RDCW 回退监听能定位索引条目的基础:句柄上的 FileIndex 与 USN 的 FRN 同口径。
/// 只要求 FILE_READ_ATTRIBUTES,普通用户也能用(目录需要 FILE_FLAG_BACKUP_SEMANTICS)。
/// </summary>
internal static bool TryStatPath(string path, out ByHandleFileInformation info)
{
info = default;
var handle = CreateFileW(
path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
if (handle.IsInvalid)
{
handle.Dispose();
return false;
}
try
{
return GetFileInformationByHandle(handle, out info);
}
finally
{
handle.Dispose();
}
}
/// <summary>把 Win32 错误码翻译成中文可读信息,供 UI 直接展示。</summary>
internal static string DescribeError(int error, string? context = null)
{
var text = error switch
{
ERROR_ACCESS_DENIED => "访问被拒绝(需要管理员权限)",
ERROR_INVALID_FUNCTION => "函数不正确(非 NTFS 卷、或无管理员权限调用 NTFS 专属 FSCTL 都会返回它)",
ERROR_NOT_SUPPORTED => "该卷不支持此操作",
ERROR_JOURNAL_NOT_ACTIVE => "该卷未启用 USN 变更日志",
ERROR_JOURNAL_DELETE_IN_PROGRESS => "USN 变更日志正在被删除",
ERROR_JOURNAL_ENTRY_DELETED => "请求的 USN 记录已被删除",
ERROR_HANDLE_EOF => "已到数据末尾",
ERROR_OPERATION_ABORTED => "操作已取消",
ERROR_CANCELLED => "操作已取消",
ERROR_NOT_READY => "卷未就绪",
_ => SafeSystemMessage(error)
};
return context is null ? text : $"{context}:{text}(Win32 错误 {error})";
}
private static string SafeSystemMessage(int error)
{
try
{
return new Win32Exception(error).Message;
}
catch
{
return "未知错误";
}
}
// ================================================================ 记录号归一化
/// <summary>MFT 记录号掩码:FRN 的低 48 位。</summary>
internal const ulong RecordNumberMask = 0x0000_FFFF_FFFF_FFFFUL;
/// <summary>剥掉 FRN 高 16 位的序列号,只保留 MFT 记录号(全索引统一用这个做键)。</summary>
internal static ulong NormalizeFrn(ulong frn) => frn & RecordNumberMask;
}
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
namespace FluidExplorer.Services.Search.Usn;
/// <summary>
/// 通配符匹配器:<c>?</c> 匹配任意单字符,<c>*</c> 匹配任意长度(含空),
/// 大小写不敏感(用固定区域的大小写折叠,不产生任何分配、不受当前区域影响)。
///
/// 实现为经典的“双指针 + 最近星号回溯”,最坏 O(n*m),但对文件名这种短串是纳秒级。
/// </summary>
public static class WildcardMatcher
{
public static bool IsMatch(ReadOnlySpan<char> text, ReadOnlySpan<char> pattern)
{
// ---- 快路径:绝大多数真实查询是 "*.json" / "log*" / "*tmp*" 这类“只有一个/两个通配符”的模式,
// 直接退化成 EndsWith/StartsWith/IndexOf(走的是 BCL 的高度优化实现),比通用回溯快一个数量级。
int stars = 0, questions = 0, firstStar = -1, lastStar = -1;
for (int i = 0; i < pattern.Length; i++)
{
var c = pattern[i];
if (c == '*')
{
stars++;
if (firstStar < 0) firstStar = i;
lastStar = i;
}
else if (c == '?')
{
questions++;
}
}
if (questions == 0)
{
switch (stars)
{
case 0:
return text.Equals(pattern, StringComparison.OrdinalIgnoreCase);
case 1 when firstStar == 0:
return text.EndsWith(pattern[1..], StringComparison.OrdinalIgnoreCase);
case 1 when firstStar == pattern.Length - 1:
return text.StartsWith(pattern[..^1], StringComparison.OrdinalIgnoreCase);
case 2 when firstStar == 0 && lastStar == pattern.Length - 1:
return text.IndexOf(pattern[1..^1], StringComparison.OrdinalIgnoreCase) >= 0;
}
}
// ---- 通用路径:双指针 + 最近星号回溯 ----
int t = 0, p = 0;
int starPattern = -1;
int starText = 0;
while (t < text.Length)
{
if (p < pattern.Length && (pattern[p] == '?' || FoldEquals(pattern[p], text[t])))
{
t++;
p++;
}
else if (p < pattern.Length && pattern[p] == '*')
{
// 记下最近的星号位置,先当它匹配空串继续往前走
starPattern = p++;
starText = t;
}
else if (starPattern >= 0)
{
// 回溯:让最近的星号多吃一个字符
p = starPattern + 1;
t = ++starText;
}
else
{
return false;
}
}
while (p < pattern.Length && pattern[p] == '*') p++;
return p == pattern.Length;
}
/// <summary>是否包含通配符。</summary>
public static bool HasWildcard(ReadOnlySpan<char> pattern) =>
pattern.IndexOfAny('*', '?') >= 0;
/// <summary>
/// 返回模式开头连续的字面量前缀(遇到 * 或 ? 为止),用于给通配符命中排优先级。
/// </summary>
public static ReadOnlySpan<char> LiteralPrefix(ReadOnlySpan<char> pattern)
{
int i = 0;
while (i < pattern.Length && pattern[i] is not ('*' or '?')) i++;
return pattern[..i];
}
/// <summary>区域无关的大小写折叠比较(只处理 ASCII 与 BMP 常见情形,足够文件名使用)。</summary>
internal static bool FoldEquals(char a, char b) =>
a == b || char.ToUpperInvariant(a) == char.ToUpperInvariant(b);
/// <summary>模式里是否只剩星号(即 "*" 或 "**" 这类恒真模式)。</summary>
internal static bool IsMatchAll(ReadOnlySpan<char> pattern)
{
foreach (var c in pattern)
{
if (c != '*') return false;
}
return true;
}
}