using System.Buffers.Binary; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text; using Microsoft.Win32.SafeHandles; namespace FluidExplorer.Services.Search.Usn; /// 增量监听实际生效的通道。 public enum IndexWatchMode { /// 未监听。 None, /// FSCTL_READ_USN_JOURNAL(首选,能拿到 FRN、改名、删除等完整信息)。 UsnJournal, /// ReadDirectoryChangesW 回退(日志不可用/无权限时;靠路径 → FileIndex 反查 FRN)。 DirectoryChanges } /// /// Everything 式的单卷 NTFS 索引。 /// /// 构建:FSCTL_ENUM_USN_DATA 一次性枚举整个 MFT 拿到“全部文件名/父目录/属性”, /// 再(若权限允许)直接读原始 $MFT 补齐真实文件大小与精确时间戳。 /// 监听:FSCTL_READ_USN_JOURNAL 阻塞式循环消费 USN 变更,墓碑标记删除、就地更新改名/大小。 /// 查询:在 Struct-of-Arrays + 大字符池上做并行扫描,候选零 string 分配,毫秒级返回 Top-N。 /// /// 线程模型: /// * 快照通过 发布;结构只增不减,读侧无需持锁。 /// * 追加/扩容走 _gate 串行化(写极少),因此读查询永远不会被写长期阻塞。 /// public sealed class UsnVolumeIndex : IFileIndex, IDisposable { private const int DefaultMaxResults = 2000; /// /// FSCTL_ENUM_USN_DATA 的输出缓冲。参考实现用 0x3900,但更大的缓冲能显著减少往返次数; /// 有些驱动对超大缓冲不友好,1MB 是实践中安全的上限。 /// private const int EnumBufferSize = 1024 * 1024; private const int WatchBufferSize = 1024 * 1024; private const long WatchBytesToWaitFor = 64 * 1024; private const int MaxPathDepth = 256; private const int MaxPathCacheEntries = 262144; private const long FileTimeToTicksOffset = 504911232000000000L; // 1601-01-01 → 0001-01-01 private const int MaxTrackedRecords = 64 * 1024 * 1024; private readonly string _devicePath; // \\.\C: private readonly string _volumeRoot; // C:\ private readonly ConcurrentDictionary _dirPathCache = new(); private readonly object _gate = new(); private IndexStore _store = new(1024); private FrnMap _frnMap = new(16); private SafeFileHandle? _volumeHandle; private MftReader? _mftReader; private Thread? _watchThread; private IntPtr _watchThreadHandle; private SafeFileHandle? _watchHandle; private int _state = (int)IndexState.NotStarted; private long _liveCount; private long _changeCount; private volatile bool _watchRequested; private volatile bool _stopRequested; private DirectoryChangeWatcher? _directoryWatcher; private bool _disposed; public UsnVolumeIndex(string volumeRoot) { (_volumeRoot, _devicePath) = NormalizeVolumeRoot(volumeRoot); } // ================================================================ IFileIndex public string VolumeRoot => _volumeRoot; public IndexState State => (IndexState)Volatile.Read(ref _state); /// 存活条目数(不含已打墓碑的删除项)。 public long EntryCount => Volatile.Read(ref _liveCount); /// 为 true 表示大小/时间来自原始 MFT 解析;false 表示 USN 降级( 多为 -1)。 public bool UsesRawMft { get; private set; } /// 降级或失败的具体原因(中文),便于 UI 直接展示。 public string? DegradationReason { get; private set; } /// 监听期间累计处理的 USN 变更条数。 public long WatchedChangeCount => Volatile.Read(ref _changeCount); /// /// 当前实际生效的增量监听通道:优先 USN 变更日志;日志不可用(无权限/不存在且建不了)时回退为 /// (ReadDirectoryChangesW,不需要日志权限)。 /// public IndexWatchMode WatchMode { get; private set; } = IndexWatchMode.None; /// /// 卷上没有 USN 日志时是否允许自动创建(FSCTL_CREATE_USN_JOURNAL,系统默认大小)。 /// 默认 true(Everything 的行为);创建属于对卷的持久改动,调用方若不愿改动系统可置 false。 /// 无论此值如何,本类从不删除日志。 /// public bool CreateJournalIfMissing { get; set; } = true; /// 索引构建是否因为找不到 $MFT 而完全没有建立(此时只有回退监听可用)。 public bool HasIndex => Volatile.Read(ref _liveCount) > 0 || Volatile.Read(ref _store).Count > 0; public event EventHandler? StateChanged; /// /// 建立索引。本方法内部使用线程池,绝不阻塞调用线程,也绝不向外抛异常: /// 权限不足 → ;非 NTFS → ; /// 取消 → 。调用方一律通过 / 判断结果。 /// public Task BuildAsync(IProgress? progress, CancellationToken cancellationToken) { // 注意:这里传 CancellationToken.None —— 取消由内部协作式处理,避免调用方拿到 Canceled 状态的 Task。 return Task.Run(() => BuildCore(progress, cancellationToken), CancellationToken.None); } public void StartWatching() { _watchRequested = true; var state = State; // 构建还没结束的话,构建完成后会自动开始监听(见 BuildCore 末尾) if (state is not (IndexState.Ready or IndexState.Watching)) return; StartWatchThread(); } public void Stop() { // 显式 Stop 之后不再自动开始监听,直到调用方再次 StartWatching() _watchRequested = false; StopWatchingInternal(); SetState(IndexState.Stopped, $"{_volumeRoot} 的索引已停止。"); } public void Dispose() { if (_disposed) return; _disposed = true; StopWatchingInternal(); try { _volumeHandle?.Dispose(); } catch (ObjectDisposedException) { } _volumeHandle = null; _mftReader = null; } // ================================================================ 构建 private void BuildCore(IProgress? progress, CancellationToken cancellationToken) { try { StopWatchingInternal(); SetState(IndexState.Building, $"正在建立 {_volumeRoot} 的文件索引…"); progress?.Report(0d); // ---- 1) 打开卷句柄:首选 GENERIC_READ|GENERIC_WRITE,失败后逐级降权 ---- var (handle, access, openError) = OpenVolumeWithBestAccess(); if (handle is null) { DegradationReason = UsnNative.DescribeError(openError, $"无法打开卷 {_devicePath}"); if (openError == UsnNative.ERROR_ACCESS_DENIED) { SetState(IndexState.RequiresElevation, "读取 NTFS 主文件表需要管理员权限,可点击以管理员身份重启索引服务。"); } else { // 其它错误(卷未就绪 / 盘符不存在 / 设备被占用…)不是权限问题,别误导用户去提权 SetState(IndexState.Failed, $"{_volumeRoot} 索引不可用:{DegradationReason}"); } return; } SafeFileHandle? old; lock (_gate) { old = _volumeHandle; _volumeHandle = handle; } old?.Dispose(); bool canReadData = (access & (UsnNative.GENERIC_READ | UsnNative.FILE_READ_DATA)) != 0; // ---- 2) 卷信息:同时用来判断是不是 NTFS ---- var volumeData = default(UsnNative.NtfsVolumeDataBuffer); bool hasVolumeData = QueryNtfsVolumeData(handle, out volumeData, out var volumeDataError); if (!hasVolumeData) { var fileSystem = TryGetFileSystemName(); if (fileSystem is not null && !fileSystem.Equals("NTFS", StringComparison.OrdinalIgnoreCase)) { DegradationReason = $"卷 {_volumeRoot} 的文件系统是 {fileSystem},不是 NTFS"; SetState(IndexState.Failed, $"{_volumeRoot} 不是 NTFS 卷(检测到 {fileSystem}),Everything 式 USN 索引仅支持 NTFS。"); return; } // 确实是 NTFS(或无法判定)却拿不到 NTFS 卷结构 —— 实测在非管理员下 // FSCTL_GET_NTFS_VOLUME_DATA 会返回 ERROR_INVALID_FUNCTION(1) 而不是 ERROR_ACCESS_DENIED(5), // 所以这里必须按“权限不足”处理,否则会把正常的 NTFS 卷误判成“不是 NTFS”。 DegradationReason = UsnNative.DescribeError(volumeDataError, "FSCTL_GET_NTFS_VOLUME_DATA 失败"); SetState(IndexState.RequiresElevation, "读取 NTFS 主文件表需要管理员权限,可点击以管理员身份重启索引服务。"); return; } // ---- 3) USN 变更日志:不存在就尝试创建(创建后绝不自动删除)---- bool hasJournal = EnsureUsnJournal(handle, out var journal, out var journalError, out bool journalCreated); if (hasJournal && journalCreated) { SetState(IndexState.Building, "本卷原先没有 USN 变更日志,已按系统默认大小创建以便做增量监听。"); } else if (!hasJournal) { DegradationReason ??= UsnNative.DescribeError(journalError, "FSCTL_QUERY_USN_JOURNAL / FSCTL_CREATE_USN_JOURNAL 失败"); } // ---- 4) 原始 MFT 读取器(需要管理员,成功则大小/时间用真实值)---- MftReader? mft = null; string? mftError = null; if (canReadData && hasVolumeData) { mft = MftReader.TryCreate(handle, in volumeData, out mftError); } else if (!canReadData) { mftError = "卷句柄不含 FILE_READ_DATA 权限(非管理员),无法直接读取 $MFT"; } else { mftError = "无法获取 NTFS 卷结构信息"; } UsesRawMft = mft is not null; if (mft is null) { DegradationReason = mftError; } // ---- 5) 枚举 MFT ---- int estimated = 65536; int bytesPerRecord = 1024; if (hasVolumeData && volumeData.BytesPerFileRecordSegment > 0) { bytesPerRecord = (int)volumeData.BytesPerFileRecordSegment; if (volumeData.MftValidDataLength > 0) { long total = volumeData.MftValidDataLength / bytesPerRecord; if (total > 0 && total <= MaxTrackedRecords) estimated = (int)total; else if (total > MaxTrackedRecords) estimated = MaxTrackedRecords; } } var store = new IndexStore(estimated); var map = new FrnMap(Math.Min(estimated, 1 << 20)); int live = 0; string? degradedNote = null; // 枚举阶段占总进度的 65%,原始 MFT 补齐阶段占 35% double enumWeight = mft is null ? 1.0 : 0.65; long reportTicks = Environment.TickCount64; // 输入按参考实现取 [FirstUsn, NextUsn];若一条都没枚举到(日志刚建好、区间过窄等), // 再退一次 [0, MAXLONGLONG] 全量范围 —— 那条路径必然能拿到整个 MFT。 long lowUsn = hasJournal ? journal.FirstUsn : 0; long highUsn = hasJournal ? journal.NextUsn : long.MaxValue; long visited = EnumMft(handle, lowUsn, highUsn, estimated, cancellationToken, progress, enumWeight, ref reportTicks, (ReadOnlySpan name, ulong frn, ulong parent, uint attributes, long timeStamp) => { bool isDir = (attributes & UsnNative.FILE_ATTRIBUTE_DIRECTORY) != 0; int idx = AppendEntry(ref store, frn, parent, name, isDir, isDir ? 0 : -1, FileTimeToTicks(timeStamp), attributes); map.AddBuild(frn, idx); live++; }); if (visited == 0 && (lowUsn != 0 || highUsn != long.MaxValue)) { degradedNote = "USN 区间 [FirstUsn, NextUsn] 未枚举到记录,已改用 [0, MAXLONGLONG] 全量枚举"; visited = EnumMft(handle, 0, long.MaxValue, estimated, cancellationToken, progress, enumWeight, ref reportTicks, (ReadOnlySpan name, ulong frn, ulong parent, uint attributes, long timeStamp) => { bool isDir = (attributes & UsnNative.FILE_ATTRIBUTE_DIRECTORY) != 0; int idx = AppendEntry(ref store, frn, parent, name, isDir, isDir ? 0 : -1, FileTimeToTicks(timeStamp), attributes); map.AddBuild(frn, idx); live++; }); } progress?.Report(enumWeight); // ---- 6) 用原始 MFT 给每条记录补齐真实大小/时间/属性 ---- if (mft is not null) { long totalBytes = mft.MftValidDataLength; mft.Enumerate( (ulong recordNumber, Span record) => { if (!MftReader.TryParseRecord(record, mft.BytesPerSector, out var info)) return; if (!info.InUse) return; if (!map.TryGet(recordNumber, out int idx)) return; if ((uint)idx >= (uint)store.Count) return; store.ModifiedTicks[idx] = FileTimeToTicks(info.ModifiedFileTime); store.Attributes[idx] = info.Attributes; if (info.IsDirectory) store.Size[idx] = 0; else if (info.Size >= 0) store.Size[idx] = info.Size; }, totalBytes, bytesRead => { long now = Environment.TickCount64; if (now - reportTicks < 60) return; reportTicks = now; double ratio = totalBytes > 0 ? Math.Min(1d, (double)bytesRead / totalBytes) : 0d; progress?.Report(enumWeight + (1d - enumWeight) * ratio); }, cancellationToken); } // ---- 7) 发布新快照 ---- // 注意发布顺序:先写 _frnMap(release),最后才用 volatile 写 _store 作为整批数据的发布点。 map.Optimize(); lock (_gate) { Volatile.Write(ref _frnMap, map); Volatile.Write(ref _liveCount, live); _mftReader = mft; _changeCount = 0; Volatile.Write(ref _store, store); } _dirPathCache.Clear(); if (degradedNote is not null) { DegradationReason = DegradationReason is null ? degradedNote : $"{DegradationReason};{degradedNote}"; } progress?.Report(1d); string summary = mft is null ? $"索引完成:{live:N0} 项(USN 降级模式,文件大小未知)。" : $"索引完成:{live:N0} 项(已解析原始 MFT,大小/时间精确)。"; SetState(IndexState.Ready, summary); // ---- 8) 构建前就调用过 StartWatching() 的话,这里补上 ---- // 注意:无论日志是否可用都要起监听线程 —— 线程内部会在日志不可用时回退到 ReadDirectoryChangesW。 if (_watchRequested) StartWatchThread(); } catch (OperationCanceledException) { SetState(IndexState.Stopped, "索引构建已取消。"); } catch (VolumeAccessException ex) { // 卷句柄能打开、但 FSCTL 被拒:同样是权限问题(非管理员下内核返回 INVALID_FUNCTION) DegradationReason = ex.Message; SetState(IndexState.RequiresElevation, "读取 NTFS 主文件表需要管理员权限,可点击以管理员身份重启索引服务。"); } catch (Exception ex) { DegradationReason = ex.Message; SetState(IndexState.Failed, $"{_volumeRoot} 索引构建失败:{ex.Message}"); } } /// FSCTL_ENUM_USN_DATA 主循环:按 StartFileReferenceNumber 递增游标直到 ERROR_HANDLE_EOF。 private long EnumMft( SafeFileHandle handle, long lowUsn, long highUsn, int estimatedRecords, CancellationToken cancellationToken, IProgress? progress, double weight, ref long reportTicks, EnumSink sink) { // MFT_ENUM_DATA_V0{ StartFileReferenceNumber = 0, LowUsn = ujd.FirstUsn, HighUsn = ujd.NextUsn } var input = new UsnNative.MftEnumDataV0 { StartFileReferenceNumber = 0, LowUsn = lowUsn, HighUsn = highUsn }; var inBuffer = new byte[Marshal.SizeOf()]; var outBuffer = new byte[EnumBufferSize]; long visited = 0; while (true) { cancellationToken.ThrowIfCancellationRequested(); WriteStruct(inBuffer, in input); if (!UsnNative.Ioctl(handle, UsnNative.FSCTL_ENUM_USN_DATA, inBuffer, outBuffer, out int bytesReturned)) { int error = Marshal.GetLastWin32Error(); // 枚举到尾时内核返回 ERROR_HANDLE_EOF,属于正常结束,不是错误 if (error == UsnNative.ERROR_HANDLE_EOF) break; throw new VolumeAccessException(error, UsnNative.DescribeError(error, "FSCTL_ENUM_USN_DATA 枚举 MFT 失败")); } // 输出缓冲最开头的 8 字节就是下一页游标(ulong),记录数据从 +8 处开始 if (bytesReturned <= 8) break; ulong nextStart = BinaryPrimitives.ReadUInt64LittleEndian(outBuffer); ParseEnumBuffer(outBuffer.AsSpan(8, bytesReturned - 8), sink, ref visited); if (nextStart == 0 || nextStart == input.StartFileReferenceNumber) break; input.StartFileReferenceNumber = nextStart; long now = Environment.TickCount64; if (now - reportTicks >= 60) { reportTicks = now; double ratio = estimatedRecords > 0 ? Math.Min(1d, (double)visited / estimatedRecords) : 0d; progress?.Report(weight * ratio); } } progress?.Report(weight); return visited; } /// FSCTL_ENUM_USN_DATA 输出缓冲解析回调(internal 以便被合成数据集测试直接驱动)。 internal delegate void EnumSink(ReadOnlySpan name, ulong frn, ulong parent, uint attributes, long timeStamp); internal static void ParseEnumBuffer(ReadOnlySpan records, EnumSink sink, ref long visited) { int offset = 0; while (offset + UsnNative.UsnRecordV2.Size <= records.Length) { var header = MemoryMarshal.Read(records[offset..]); int recordLength = (int)header.RecordLength; if (recordLength < UsnNative.UsnRecordV2.Size || offset + recordLength > records.Length) break; visited++; // USN_RECORD_V2 之外(V3/V4)在 NTFS 卷上不会出现;这里只处理 V2 if (header.MajorVersion == 2 && header.FileNameLength > 0 && header.FileNameOffset + header.FileNameLength <= recordLength) { var name = MemoryMarshal.Cast( records.Slice(offset + header.FileNameOffset, header.FileNameLength)); // 卷根目录的名字是 ".",父目录自指,入索引没有意义 if (!(name.Length == 1 && name[0] == '.')) { sink(name, UsnNative.NormalizeFrn(header.FileReferenceNumber), UsnNative.NormalizeFrn(header.ParentFileReferenceNumber), header.FileAttributes, header.TimeStamp); } } offset += recordLength; } } // ================================================================ 增量监听 private void StartWatchThread() { lock (_gate) { if (_disposed) return; if (_watchThread is { IsAlive: true }) return; _stopRequested = false; var thread = new Thread(WatchLoop) { IsBackground = true, Name = $"UsnWatch[{_volumeRoot}]", Priority = ThreadPriority.BelowNormal }; _watchThread = thread; thread.Start(); } } /// /// FSCTL_READ_USN_JOURNAL 阻塞式循环。BytesToWaitFor=64KB 让内核攒够一批再唤醒, /// 每次唤醒只处理一个批次,改完一次性触发 StateChanged(Watching)。 /// private void WatchLoop() { SafeFileHandle? handle = null; try { _watchThreadHandle = UsnNative.OpenThread(UsnNative.THREAD_TERMINATE, false, UsnNative.GetCurrentThreadId()); var (opened, _, openError) = OpenVolumeWithBestAccess(); handle = opened; bool needFallback; if (handle is null) { ReportWatchIssue($"无法打开卷句柄用于增量监听({UsnNative.DescribeError(openError)}),改用 ReadDirectoryChangesW 回退监听。"); needFallback = true; } else { Volatile.Write(ref _watchHandle, handle); // 日志不存在就尝试创建(需要管理员);拿不到就回退 if (EnsureUsnJournal(handle, out var journal, out int journalError, out bool created)) { if (created) { ReportWatchIssue("本卷原先没有 USN 变更日志,已按系统默认大小创建(不会自动删除)以便做增量监听。"); } needFallback = RunUsnJournalLoop(handle, in journal); } else { ReportWatchIssue($"USN 变更日志不可用({UsnNative.DescribeError(journalError)}),改用 ReadDirectoryChangesW 回退监听。"); needFallback = true; } } if (needFallback && !_stopRequested) RunDirectoryChangesLoop(); } catch (Exception ex) { if (!_stopRequested) SetState(IndexState.Ready, $"实时监听异常退出:{ex.Message}"); } finally { var watchHandle = Interlocked.Exchange(ref _watchHandle, null); watchHandle?.Dispose(); handle?.Dispose(); var threadHandle = Interlocked.Exchange(ref _watchThreadHandle, IntPtr.Zero); if (threadHandle != IntPtr.Zero) UsnNative.CloseHandle(threadHandle); } } /// /// FSCTL_READ_USN_JOURNAL 阻塞循环。返回 true 表示「USN 通道不可用,请回退到 RDCW」; /// false 表示正常收到停止信号退出。 /// private bool RunUsnJournalLoop(SafeFileHandle handle, in UsnNative.UsnJournalDataV0 journal) { var read = new UsnNative.ReadUsnJournalDataV0 { StartUsn = journal.NextUsn, ReasonMask = UsnNative.USN_REASON_ANY, ReturnOnlyOnClose = 0, Timeout = 0, // 0 = 无数据时无限等待,靠 CancelSynchronousIo 打断 BytesToWaitFor = WatchBytesToWaitFor, UsnJournalID = journal.UsnJournalID }; var inBuffer = new byte[Marshal.SizeOf()]; var outBuffer = new byte[WatchBufferSize]; WatchMode = IndexWatchMode.UsnJournal; SetState(IndexState.Watching, $"{_volumeRoot} 索引已就绪,正在通过 USN 日志实时监听文件变更。"); while (!_stopRequested) { WriteStruct(inBuffer, in read); bool ok = UsnNative.Ioctl(handle, UsnNative.FSCTL_READ_USN_JOURNAL, inBuffer, outBuffer, out int bytesReturned); if (!ok) { int error = Marshal.GetLastWin32Error(); if (_stopRequested || error is UsnNative.ERROR_OPERATION_ABORTED or UsnNative.ERROR_CANCELLED or UsnNative.ERROR_INVALID_HANDLE) { return false; } // 日志被删除/重建:重新确保日志可用后继续(期间可能漏掉少量变更,重建索引可修正) if (error is UsnNative.ERROR_JOURNAL_DELETE_IN_PROGRESS or UsnNative.ERROR_JOURNAL_NOT_ACTIVE or UsnNative.ERROR_JOURNAL_ENTRY_DELETED) { if (!EnsureUsnJournal(handle, out var refreshed, out _, out _)) { ReportWatchIssue("USN 日志已被删除且无法重新获取,改用 ReadDirectoryChangesW 回退监听。"); return true; } read.StartUsn = refreshed.FirstUsn; read.UsnJournalID = refreshed.UsnJournalID; continue; } ReportWatchIssue($"USN 实时监听中断({UsnNative.DescribeError(error)}),改用 ReadDirectoryChangesW 回退监听。"); return true; } if (bytesReturned <= 8) continue; read.StartUsn = BinaryPrimitives.ReadInt64LittleEndian(outBuffer); int applied = ApplyChanges(outBuffer.AsSpan(8, bytesReturned - 8)); if (applied > 0) { Interlocked.Add(ref _changeCount, applied); SetState(IndexState.Watching, $"{_volumeRoot} 索引已更新({applied} 条变更)。"); } } return false; } /// ReadDirectoryChangesW 回退监听(不需要 USN 日志权限)。 private void RunDirectoryChangesLoop() { var watcher = new DirectoryChangeWatcher(this); if (!watcher.TryOpen(out var error)) { WatchMode = IndexWatchMode.None; SetState(IndexState.Ready, $"实时监听不可用:{error}"); return; } _directoryWatcher = watcher; WatchMode = IndexWatchMode.DirectoryChanges; SetState(IndexState.Watching, $"{_volumeRoot} 索引已就绪(USN 日志不可用,回退为 ReadDirectoryChangesW 实时监听)。"); try { watcher.Run(); } finally { _directoryWatcher = null; } if (!_stopRequested) { WatchMode = IndexWatchMode.None; SetState(IndexState.Ready, "实时监听已停止。"); } } /// 应用一批 USN 变更记录,返回处理条数(internal 以便被合成数据集测试直接驱动)。 internal int ApplyChanges(ReadOnlySpan records) { int offset = 0; int applied = 0; while (offset + UsnNative.UsnRecordV2.Size <= records.Length) { var header = MemoryMarshal.Read(records[offset..]); int recordLength = (int)header.RecordLength; if (recordLength < UsnNative.UsnRecordV2.Size || offset + recordLength > records.Length) break; if (header.MajorVersion == 2) { ReadOnlySpan name = default; if (header.FileNameLength > 0 && header.FileNameOffset + header.FileNameLength <= recordLength) { name = MemoryMarshal.Cast( records.Slice(offset + header.FileNameOffset, header.FileNameLength)); } ApplyOne(in header, name); applied++; } offset += recordLength; } return applied; } private void ApplyOne(in UsnNative.UsnRecordV2 header, ReadOnlySpan name) { ulong frn = UsnNative.NormalizeFrn(header.FileReferenceNumber); ulong parent = UsnNative.NormalizeFrn(header.ParentFileReferenceNumber); uint reason = header.Reason; var store = Volatile.Read(ref _store); bool exists = _frnMap.TryGet(frn, out int index) && (uint)index < (uint)store.Count; // ---- 删除:打墓碑,不搬移数组 ---- if ((reason & UsnNative.USN_REASON_FILE_DELETE) != 0) { if (exists && (store.Flags[index] & IndexStore.FlagDeleted) == 0) { store.Flags[index] |= IndexStore.FlagDeleted; Interlocked.Decrement(ref _liveCount); } return; } bool isDir = (header.FileAttributes & UsnNative.FILE_ATTRIBUTE_DIRECTORY) != 0; if (!exists) { if (name.Length == 0) return; index = AppendEntryThreadSafe(frn, parent, name, isDir, isDir ? 0 : -1, FileTimeToTicks(header.TimeStamp), header.FileAttributes); Interlocked.Increment(ref _liveCount); RefreshFromDisk(frn, index); return; } // ---- 改名 / 移动:就地改名字引用与父目录(NameRef 单次 8 字节原子写,读侧不会看到半新半旧)---- if (name.Length > 0) { var current = store.GetName(index); if (!current.SequenceEqual(name)) { if (name.Length > NamePool.MaxNameLength) name = name[..NamePool.MaxNameLength]; int nameOffset = store.Names.Add(name); Volatile.Write(ref store.NameRef[index], NamePool.Pack(nameOffset, name.Length)); store.ParentFrn[index] = parent; } else if (store.ParentFrn[index] != parent) { store.ParentFrn[index] = parent; } } // 复活:之前被标记删除的记录号又被新文件复用了 if ((store.Flags[index] & IndexStore.FlagDeleted) != 0 && (reason & UsnNative.USN_REASON_FILE_CREATE) != 0) { store.Flags[index] &= unchecked((byte)~IndexStore.FlagDeleted); Interlocked.Increment(ref _liveCount); } RefreshFromDisk(frn, index); } /// 用原始 MFT 刷新单个条目的大小/时间/属性;没有 MFT 时退化为用 USN 的变更时间。 private void RefreshFromDisk(ulong frn, int index) { var store = Volatile.Read(ref _store); if ((uint)index >= (uint)store.Count) return; var mft = _mftReader; if (mft is null) return; // USN 降级模式:大小保持 -1(未知),时间已在创建/改名时写入 Span buffer = mft.BytesPerRecord <= 4096 ? stackalloc byte[4096] : new byte[mft.BytesPerRecord]; if (!mft.TryReadRecord(frn, buffer)) return; if (!MftReader.TryParseRecord(buffer, mft.BytesPerSector, out var info)) return; if (info.IsDirectory) store.Size[index] = 0; else if (info.Size >= 0) store.Size[index] = info.Size; if (info.ModifiedFileTime > 0) store.ModifiedTicks[index] = FileTimeToTicks(info.ModifiedFileTime); if (info.Attributes != 0) store.Attributes[index] = info.Attributes; } // ================================================================ RDCW 回退通道的落地接口 /// /// 由 调用:按 FRN 写入/更新一条记录(RDCW 只有路径,FRN 来自句柄)。 /// internal void UpsertFromFileSystem(ulong frn, ulong parentFrn, ReadOnlySpan name, bool isDirectory, long size, long modifiedTicks, uint attributes) { frn = UsnNative.NormalizeFrn(frn); parentFrn = UsnNative.NormalizeFrn(parentFrn); var store = Volatile.Read(ref _store); if (_frnMap.TryGet(frn, out int index) && (uint)index < (uint)store.Count) { if (name.Length > 0) { var current = store.GetName(index); if (!current.SequenceEqual(name)) { if (name.Length > NamePool.MaxNameLength) name = name[..NamePool.MaxNameLength]; int nameOffset = store.Names.Add(name); Volatile.Write(ref store.NameRef[index], NamePool.Pack(nameOffset, name.Length)); } if (parentFrn != 0) store.ParentFrn[index] = parentFrn; } store.Size[index] = isDirectory ? 0 : size; if (modifiedTicks > 0) store.ModifiedTicks[index] = modifiedTicks; if (attributes != 0) store.Attributes[index] = attributes; store.Flags[index] = (byte)((store.Flags[index] & IndexStore.FlagDeleted) | (isDirectory ? IndexStore.FlagDirectory : 0)); // 记录号被复用:之前打的墓碑要撤掉 if ((store.Flags[index] & IndexStore.FlagDeleted) != 0) { store.Flags[index] &= unchecked((byte)~IndexStore.FlagDeleted); Interlocked.Increment(ref _liveCount); } Interlocked.Increment(ref _changeCount); return; } if (name.Length == 0) return; AppendEntryThreadSafe(frn, parentFrn, name, isDirectory, isDirectory ? 0 : size, modifiedTicks, attributes); Interlocked.Increment(ref _liveCount); Interlocked.Increment(ref _changeCount); } /// /// 由 调用:对某个目录做「磁盘现状 vs 索引」对账, /// 把索引里存在、磁盘上已消失的孩子打上墓碑。返回打墓碑的条数。 /// RDCW 的删除事件只给路径、无法反查 FRN,所以删除统一走这条对账路径。 /// internal int ResyncDirectoryFromDisk(string directoryPath) { if (!UsnNative.TryStatPath(directoryPath, out var directoryInfo)) return 0; ulong directoryFrn = UsnNative.NormalizeFrn(directoryInfo.FileIndex); if (directoryFrn == 0) return 0; HashSet liveNames; try { liveNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entry in Directory.EnumerateFileSystemEntries(directoryPath)) { var leaf = Path.GetFileName(entry); if (leaf.Length > 0) liveNames.Add(leaf); } } catch (IOException) { return 0; } catch (UnauthorizedAccessException) { return 0; } var store = Volatile.Read(ref _store); int tombstoned = 0; for (int i = 0; i < store.Count; i++) { if ((store.Flags[i] & IndexStore.FlagDeleted) != 0) continue; if (store.ParentFrn[i] != directoryFrn) continue; var name = store.GetName(i); if (name.Length == 0) continue; if (liveNames.Contains(name.ToString())) continue; store.Flags[i] |= IndexStore.FlagDeleted; Interlocked.Decrement(ref _liveCount); Interlocked.Increment(ref _changeCount); tombstoned++; } return tombstoned; } /// 把监听通道的异常/进展以 StateChanged(Watching) 的形式抛给 UI。 internal void ReportWatchIssue(string? message, int tombstoned = 0) { if (message is null && tombstoned <= 0) return; var text = message is null ? $"{_volumeRoot} 索引已更新({tombstoned} 项删除)。" : tombstoned > 0 ? $"{message}({tombstoned} 项删除)" : message; SetState(WatchMode == IndexWatchMode.None ? IndexState.Ready : IndexState.Watching, text); } private void StopWatchingInternal() { _stopRequested = true; Thread? thread; lock (_gate) { thread = _watchThread; _watchThread = null; } var threadHandle = _watchThreadHandle; if (threadHandle != IntPtr.Zero) UsnNative.CancelSynchronousIo(threadHandle); // 回退通道用的是目录句柄上的 ReadDirectoryChangesW,同样用 CancelSynchronousIo 打断,外加关句柄兜底 _directoryWatcher?.RequestStop(); if (thread is { IsAlive: true }) { if (!thread.Join(1500)) { // 兜底:CancelSynchronousIo 没生效时直接关掉卷句柄,让阻塞中的 DeviceIoControl 立刻失败返回 var watchHandle = Interlocked.Exchange(ref _watchHandle, null); watchHandle?.Dispose(); UsnNative.CancelSynchronousIo(threadHandle); thread.Join(1500); } } } // ================================================================ 查询 public IEnumerable Query(SearchQuery query, int maxResults, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query); var store = Volatile.Read(ref _store); int count = store.Count; int k = maxResults > 0 ? maxResults : DefaultMaxResults; if (count == 0) return []; var spec = new QuerySpec(query); // 并行分片:分片数按 CPU 核数放大,同时限制每片 Top-K 堆的总内存 int partitions = Math.Min(count, Math.Max(1, Environment.ProcessorCount * 4)); if (partitions > 1 && (long)k * partitions > 4_000_000L) { partitions = (int)Math.Max(1, 4_000_000L / k); } int chunk = (count + partitions - 1) / partitions; var partials = new TopK?[partitions]; var options = new ParallelOptions { CancellationToken = cancellationToken, MaxDegreeOfParallelism = Environment.ProcessorCount }; Parallel.For(0, partitions, options, p => { int start = p * chunk; int end = Math.Min(count, start + chunk); if (start >= end) return; var top = new TopK(k); for (int i = start; i < end; i++) { long key = Probe(store, i, in spec); if (key >= 0) top.Add(key, i); } partials[p] = top; }); var merged = new TopK(k); foreach (var partial in partials) { partial?.DrainInto(merged); } var hits = merged.ToSortedArray(); var results = new List(hits.Length); foreach (var (key, index) in hits) { cancellationToken.ThrowIfCancellationRequested(); var entry = Materialize(store, index); if (entry is not null) results.Add(entry.Value); } return results; } /// /// 单条候选的过滤 + 打分。返回 >=0 的排序键(越小越好),-1 表示被过滤掉。 /// 过滤顺序刻意从最便宜的条件到最贵的:先看标志位/大小/时间,再做名字匹配,最后才解析路径。 /// private long Probe(IndexStore store, int i, in QuerySpec spec) { byte flags = store.Flags[i]; if ((flags & IndexStore.FlagDeleted) != 0) return -1; bool isDir = (flags & IndexStore.FlagDirectory) != 0; if (spec.DirectoriesOnly && !isDir) return -1; if (spec.FilesOnly && isDir) return -1; long size = store.Size[i]; if (size >= 0) { // 注意:Size < 0 表示“未知”(USN 降级),必须放行,绝不能当成 0 字节 if (spec.MinSize != long.MinValue && size < spec.MinSize) return -1; if (spec.MaxSize != long.MaxValue && size > spec.MaxSize) return -1; } long ticks = store.ModifiedTicks[i]; if (ticks > 0) { if (spec.ModifiedAfterTicks > 0 && ticks < spec.ModifiedAfterTicks) return -1; if (spec.ModifiedBeforeTicks > 0 && ticks > spec.ModifiedBeforeTicks) return -1; } var name = store.GetName(i); if (spec.Extensions.Length > 0 && !MatchExtension(name, spec.Extensions)) return -1; if (spec.ExcludeExtensions.Length > 0 && MatchExtension(name, spec.ExcludeExtensions)) return -1; string? path = null; if (!MatchNameAll(name, in spec, out int rank)) { // 名字没全中:只有允许整路径匹配时才付出解析路径的代价 if (!spec.MatchWholePath) return -1; path = ResolveFullPath(i); if (path is null || !MatchNameAll(path.AsSpan(), in spec, out rank)) return -1; } if (spec.PathFilter is not null) { path ??= ResolveFullPath(i); if (path is null) return -1; if (path.IndexOf(spec.PathFilter, StringComparison.OrdinalIgnoreCase) < 0) return -1; } return PackKey(rank, name.Length, i); } private static long PackKey(int rank, int nameLength, int index) => ((long)rank << 56) | ((long)Math.Min(nameLength, 0xFFFFFF) << 32) | (uint)index; /// IncludeTerms / IncludeRegexLike 全部命中才算通过;rank 取最差的一档。 private static bool MatchNameAll(ReadOnlySpan name, in QuerySpec spec, out int rank) { rank = 0; var terms = spec.IncludeTerms; for (int t = 0; t < terms.Length; t++) { int r = MatchLiteral(name, terms[t]); if (r < 0) return false; if (r > rank) rank = r; } var patterns = spec.Wildcards; for (int t = 0; t < patterns.Length; t++) { var pattern = patterns[t]; if (!WildcardMatcher.IsMatch(name, pattern)) return false; int r = WildcardRank(name, pattern); if (r > rank) rank = r; } var excludes = spec.ExcludeTerms; for (int t = 0; t < excludes.Length; t++) { if (name.IndexOf(excludes[t], StringComparison.OrdinalIgnoreCase) >= 0) return false; } return true; } /// 字面量子串匹配并给出优先级:完全相等(0) > 前缀(1) > 词边界(2) > 普通子串(3)。 private static int MatchLiteral(ReadOnlySpan name, string term) { int idx = name.IndexOf(term, StringComparison.OrdinalIgnoreCase); if (idx < 0) return -1; if (idx != 0) return !char.IsLetterOrDigit(name[idx - 1]) ? 2 : 3; return name.Length == term.Length ? 0 : 1; } private static int WildcardRank(ReadOnlySpan name, ReadOnlySpan pattern) { var prefix = WildcardMatcher.LiteralPrefix(pattern); if (prefix.Length == 0) return 3; if (!name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return 3; return name.Length == prefix.Length ? 0 : 1; } /// 扩展名比较:忽略大小写,且允许查询侧不带点("log" 与 ".LOG" 都算命中)。 private static bool MatchExtension(ReadOnlySpan name, string[] extensions) { int dot = name.LastIndexOf('.'); if (dot <= 0 || dot == name.Length - 1) return false; var extension = name[(dot + 1)..]; foreach (var candidate in extensions) { if (extension.Equals(candidate, StringComparison.OrdinalIgnoreCase)) return true; } return false; } private IndexedEntry? Materialize(IndexStore store, int index) { if ((uint)index >= (uint)store.Count) return null; long nameRef = Volatile.Read(ref store.NameRef[index]); var nameSpan = store.Names.Get(NamePool.UnpackOffset(nameRef), NamePool.UnpackLength(nameRef)); if (nameSpan.Length == 0) return null; bool isDir = (store.Flags[index] & IndexStore.FlagDirectory) != 0; long ticks = store.ModifiedTicks[index]; var modified = ticks > 0 ? new DateTime(ticks, DateTimeKind.Utc) : default; return new IndexedEntry( store.Frn[index], store.ParentFrn[index], nameSpan.ToString(), isDir, store.Size[index], modified, store.Attributes[index]) { FullPath = ResolveFullPath(index) }; } // ================================================================ 路径解析 public bool TryResolvePath(ulong frn, out string fullPath) { fullPath = string.Empty; if (!_frnMap.TryGet(frn, out int index)) return false; var store = Volatile.Read(ref _store); if ((uint)index >= (uint)store.Count) return false; var path = ResolveFullPath(index); if (path is null) return false; fullPath = path; return true; } /// 条目 → 完整路径。父链缺失或 FRN==0 时以 终止;彻底失败返回 null。 private string? ResolveFullPath(int index) { var store = Volatile.Read(ref _store); if ((uint)index >= (uint)store.Count) return null; bool isDir = (store.Flags[index] & IndexStore.FlagDirectory) != 0; if (isDir) return ResolveDirectoryPath(index); var name = store.GetName(index); if (name.Length == 0) return null; ulong frn = store.Frn[index]; ulong parent = store.ParentFrn[index]; if (parent == 0 || parent == frn) return string.Concat(VolumeRoot, name); if (!_frnMap.TryGet(parent, out int parentIndex) || (uint)parentIndex >= (uint)store.Count) { return string.Concat(VolumeRoot, name); } var directory = ResolveDirectoryPath(parentIndex); return directory is null ? null : string.Concat(directory, name); } /// /// 目录条目 → 以 '\' 结尾的完整路径。父链结果整体缓存(键是目录 FRN), /// 上层目录一旦算过,子目录拼一次字符串即可 —— 这是路径过滤能保持毫秒级的关键。 /// private string? ResolveDirectoryPath(int index) { var store = Volatile.Read(ref _store); if ((uint)index >= (uint)store.Count) return null; ulong startFrn = store.Frn[index]; if (_dirPathCache.TryGetValue(startFrn, out var cached)) { return cached.Length == 0 ? null : cached; } List? chain = null; string? prefix = null; int current = index; for (int depth = 0; depth < MaxPathDepth; depth++) { ulong frn = store.Frn[current]; ulong parent = store.ParentFrn[current]; // 终止条件(三条,都不硬编码根 FRN): // (a) 父指向自身 —— NTFS 卷根(MFT 记录 5)的特征; // (b) parent == 0(没有父); // (c) 父不在映射里(父已被删/记录号已复用)。 // 命中任一条就以卷符为前缀结束,并且当前这一层(卷根自己)不写进路径。 if (parent == 0 || parent == frn) { prefix = VolumeRoot; break; } (chain ??= new List(8)).Add(current); if (_dirPathCache.TryGetValue(parent, out var parentPath)) { if (parentPath.Length == 0) { CacheDirectoryPath(startFrn, string.Empty); return null; } prefix = parentPath; break; } if (!_frnMap.TryGet(parent, out int parentIndex) || (uint)parentIndex >= (uint)store.Count) { prefix = VolumeRoot; // 父项缺失 → 以卷根终止 break; } current = parentIndex; } if (prefix is null) { // 超过深度上限:父链成环或数据损坏 CacheDirectoryPath(startFrn, string.Empty); return null; } var builder = new StringBuilder(prefix, prefix.Length + chain!.Count * 16); for (int i = chain.Count - 1; i >= 0; i--) { var segment = store.GetName(chain[i]); if (segment.Length == 0) continue; // 卷根目录在枚举里可能叫 "."(也可能是卷标),两种都不应该出现在路径里 if (segment.Length == 1 && segment[0] == '.') continue; if (segment.Length == 2 && segment[0] == '.' && segment[1] == '.') continue; builder.Append(segment).Append('\\'); } var path = builder.ToString(); CacheDirectoryPath(startFrn, path); return path; } private void CacheDirectoryPath(ulong frn, string path) { if (_dirPathCache.Count >= MaxPathCacheEntries) _dirPathCache.Clear(); _dirPathCache[frn] = path; } // ================================================================ 卷 / 日志 /// /// 打开卷句柄。 /// /// 首选参数严格按参考实现(Everything)来: /// CreateFileW(@"\\.\C:", GENERIC_READ|GENERIC_WRITE, /// FILE_SHARE_READ|FILE_SHARE_WRITE, null, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, null) /// —— 共享模式必须带 FILE_SHARE_WRITE;dwFlagsAndAttributes 用 FILE_ATTRIBUTE_READONLY(NORMAL 反而可能开不了)。 /// /// 之后逐级降权重试是为了「非管理员也能跑」:普通用户开不了读写卷句柄, /// 但常常仍能拿到 FILE_READ_ATTRIBUTES 的句柄,从而完成 MFT 枚举(只是读不了原始数据 → 大小未知)。 /// private (SafeFileHandle? Handle, uint Access, int Error) OpenVolumeWithBestAccess() { ReadOnlySpan accesses = [ UsnNative.GENERIC_READ | UsnNative.GENERIC_WRITE, UsnNative.GENERIC_READ, UsnNative.FILE_READ_DATA | UsnNative.FILE_READ_ATTRIBUTES, UsnNative.FILE_READ_ATTRIBUTES, 0 ]; const uint share = UsnNative.FILE_SHARE_READ | UsnNative.FILE_SHARE_WRITE | UsnNative.FILE_SHARE_DELETE; const uint flags = UsnNative.FILE_ATTRIBUTE_READONLY; int lastError = 0; foreach (var access in accesses) { var handle = UsnNative.CreateFileW( _devicePath, access, share, IntPtr.Zero, UsnNative.OPEN_EXISTING, flags, IntPtr.Zero); if (!handle.IsInvalid) return (handle, access, 0); lastError = Marshal.GetLastWin32Error(); handle.Dispose(); } return (null, 0, lastError); } private static bool QueryNtfsVolumeData(SafeFileHandle handle, out UsnNative.NtfsVolumeDataBuffer data, out int error) { data = default; var buffer = new byte[Marshal.SizeOf()]; if (UsnNative.Ioctl(handle, UsnNative.FSCTL_GET_NTFS_VOLUME_DATA, [], buffer, out int bytes) && bytes >= buffer.Length) { data = MemoryMarshal.Read(buffer); error = 0; return true; } error = Marshal.GetLastWin32Error(); return false; } private static bool QueryUsnJournal(SafeFileHandle handle, out UsnNative.UsnJournalDataV0 journal, out int error) { journal = default; var buffer = new byte[Marshal.SizeOf()]; if (UsnNative.Ioctl(handle, UsnNative.FSCTL_QUERY_USN_JOURNAL, [], buffer, out int bytes) && bytes >= buffer.Length) { journal = MemoryMarshal.Read(buffer); error = 0; return journal.UsnJournalID != 0; } error = Marshal.GetLastWin32Error(); return false; } /// /// 确保卷上有 USN 变更日志可用:先 QUERY,拿不到(ERROR_JOURNAL_NOT_ACTIVE / ERROR_INVALID_FUNCTION 等) /// 就尝试 FSCTL_CREATE_USN_JOURNAL 创建(MaximumSize/AllocationDelta 传 0 = 用系统默认值)。 /// /// 重要约定:**创建后绝不自动删除**(删掉就再也没法做增量监听了)。 /// 只有调用方显式调用 才会删,且 DeleteFlags 用 USN_DELETE_FLAG_DELETE。 /// private bool EnsureUsnJournal(SafeFileHandle handle, out UsnNative.UsnJournalDataV0 journal, out int error, out bool created) { created = false; if (QueryUsnJournal(handle, out journal, out error)) return true; if (!CreateJournalIfMissing) return false; // 创建日志是需要管理员权限的操作;失败时把错误码原样带回给调用方分类 var input = new byte[Marshal.SizeOf()]; WriteStruct(input, new UsnNative.CreateUsnJournalData { MaximumSize = 0, AllocationDelta = 0 }); var output = new byte[16]; // 该 FSCTL 无输出,给个非空缓冲更保守 if (!UsnNative.Ioctl(handle, UsnNative.FSCTL_CREATE_USN_JOURNAL, input, output, out _)) { error = Marshal.GetLastWin32Error(); return false; } created = true; return QueryUsnJournal(handle, out journal, out error); } /// /// 显式删除本卷的 USN 变更日志(DeleteFlags = USN_DELETE_FLAG_DELETE)。 /// 只有「用户主动关闭本卷索引并愿意放弃增量能力」时才调用;内部任何流程都不会自动调用它。 /// public bool DeleteUsnJournal() { var (handle, _, _) = OpenVolumeWithBestAccess(); if (handle is null) return false; try { if (!QueryUsnJournal(handle, out var journal, out _)) return false; var input = new byte[Marshal.SizeOf()]; WriteStruct(input, new UsnNative.DeleteUsnJournalData { UsnJournalID = journal.UsnJournalID, DeleteFlags = UsnNative.USN_DELETE_FLAG_DELETE }); var output = new byte[16]; return UsnNative.Ioctl(handle, UsnNative.FSCTL_DELETE_USN_JOURNAL, input, output, out _); } finally { handle.Dispose(); } } /// 通过 FSCTL_GET_NTFS_FILE_RECORD 直接取一条 MFT 记录(备用通道,不依赖 ReadFile 权限)。 internal static bool TryGetNtfsFileRecord(SafeFileHandle handle, ulong recordNumber, byte[] buffer, out int recordLength) { recordLength = 0; var input = new byte[Marshal.SizeOf()]; WriteStruct(input, new UsnNative.NtfsFileRecordInputBuffer { FileReferenceNumber = UsnNative.NormalizeFrn(recordNumber) }); if (!UsnNative.Ioctl(handle, UsnNative.FSCTL_GET_NTFS_FILE_RECORD, input, buffer, out int bytes)) return false; if (bytes < Marshal.SizeOf()) return false; recordLength = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(8)); return recordLength > 0; } // ================================================================ 工具 private static unsafe void WriteStruct(byte[] buffer, in T value) where T : unmanaged { fixed (byte* p = buffer) { *(T*)p = value; } } /// /// 追加一条记录;容量不足时 copy-on-write 扩容。返回新下标(扩容后 store 引用可能被替换,故用 ref)。 /// 名字池始终取自 store.Names —— 绝不能让“写名字的池”和“快照的池”变成两个对象。 /// private static int AppendEntry(ref IndexStore store, ulong frn, ulong parent, ReadOnlySpan name, bool isDirectory, long size, long modifiedTicks, uint attributes) { if (store.Count >= store.Capacity) store = IndexStore.Grow(store, store.Count + 1); if (name.Length > NamePool.MaxNameLength) name = name[..NamePool.MaxNameLength]; int index = store.Count; int nameOffset = store.Names.Add(name); store.Frn[index] = frn; store.ParentFrn[index] = parent; store.NameRef[index] = NamePool.Pack(nameOffset, name.Length); store.Size[index] = size; store.ModifiedTicks[index] = modifiedTicks; store.Attributes[index] = attributes; store.Flags[index] = isDirectory ? IndexStore.FlagDirectory : (byte)0; // 最后才发布条数:保证读侧看到的下标一定已经写满字段 store.Count = index + 1; return index; } private int AppendEntryThreadSafe(ulong frn, ulong parent, ReadOnlySpan name, bool isDirectory, long size, long modifiedTicks, uint attributes) { lock (_gate) { var store = Volatile.Read(ref _store); int index = AppendEntry(ref store, frn, parent, name, isDirectory, size, modifiedTicks, attributes); Volatile.Write(ref _store, store); _frnMap.Set(frn, index); return index; } } private void SetState(IndexState state, string? message = null) { Volatile.Write(ref _state, (int)state); var handler = StateChanged; if (handler is null) return; try { handler(this, new IndexStateChangedEventArgs(state, message)); } catch { // UI 回调里的异常绝不能影响索引线程 } } /// FILETIME(1601-01-01 起) → .NET ticks(0001-01-01 起);非法值一律变成 0(= 未知)。 internal static long FileTimeToTicks(long fileTime) { if (fileTime <= 0) return 0; long ticks = fileTime + FileTimeToTicksOffset; if (ticks < 0 || ticks > DateTime.MaxValue.Ticks) return 0; return ticks; } /// 查询侧时间边界 → UTC ticks(SearchQuery 里的日期都是本地时间语义)。 private static long ToUtcTicks(DateTime? value) { if (value is not { } date) return 0; return date.Kind == DateTimeKind.Utc ? date.Ticks : date.ToUniversalTime().Ticks; } /// /// 不带任何权限地用 DriveInfo 读出文件系统名("NTFS"/"FAT32"/"exFAT"…)。 /// 关键作用:把“不是 NTFS”和“没有权限”这两种都会返回 ERROR_INVALID_FUNCTION 的情况区分开。 /// private string? TryGetFileSystemName() { try { if (_volumeRoot.Length >= 2 && _volumeRoot[1] == ':') return new DriveInfo(_volumeRoot).DriveFormat; } catch (IOException) { return null; } catch (UnauthorizedAccessException) { return null; } return null; } private static (string Root, string Device) NormalizeVolumeRoot(string volumeRoot) { ArgumentException.ThrowIfNullOrWhiteSpace(volumeRoot); var text = volumeRoot.Trim(); // \\?\Volume{GUID}\ 形式:直接用它当设备路径 if (text.StartsWith(@"\\?\", StringComparison.Ordinal) || text.StartsWith(@"\\.\", StringComparison.Ordinal)) { var device = text.TrimEnd('\\'); return (device + "\\", device); } var letter = char.ToUpperInvariant(text[0]); if (letter is < 'A' or > 'Z') { throw new ArgumentException($"卷根必须是盘符形式(如 \"C:\\\")或 \\\\?\\Volume{{GUID}}\\,收到:{volumeRoot}", nameof(volumeRoot)); } return ($"{letter}:\\", $@"\\.\{letter}:"); } // ================================================================ 查询预处理 / Top-K /// 把 SearchQuery 里每轮循环都要读的字段摊平,避免热路径上反复做属性/接口调用。 private readonly struct QuerySpec { internal readonly string[] IncludeTerms; internal readonly string[] ExcludeTerms; internal readonly string[] Wildcards; internal readonly string[] Extensions; internal readonly string[] ExcludeExtensions; internal readonly string? PathFilter; internal readonly bool MatchWholePath; internal readonly bool DirectoriesOnly; internal readonly bool FilesOnly; internal readonly long MinSize; internal readonly long MaxSize; internal readonly long ModifiedAfterTicks; internal readonly long ModifiedBeforeTicks; internal QuerySpec(SearchQuery query) { IncludeTerms = [.. query.IncludeTerms]; ExcludeTerms = [.. query.ExcludeTerms]; Wildcards = [.. query.IncludeRegexLike]; Extensions = [.. query.Extensions]; ExcludeExtensions = [.. query.ExcludeExtensions]; PathFilter = string.IsNullOrEmpty(query.PathFilter) ? null : query.PathFilter; MatchWholePath = query.MatchWholePath || PathFilter is not null; DirectoriesOnly = query.DirectoriesOnly; FilesOnly = query.FilesOnly; MinSize = query.MinSize ?? long.MinValue; MaxSize = query.MaxSize ?? long.MaxValue; ModifiedAfterTicks = ToUtcTicks(query.ModifiedAfter); ModifiedBeforeTicks = ToUtcTicks(query.ModifiedBefore); // 说明:SearchQuery.CreatedAfter 无法参与过滤 —— IndexedEntry 没有创建时间字段 //($STANDARD_INFORMATION 的创建时间只在原始 MFT 解析里拿得到),需要接口扩展才能支持。 } } /// 卷句柄可用但 FSCTL 被内核拒绝(权限/非 NTFS),携带原始 Win32 错误码供上层分类。 private sealed class VolumeAccessException(int error, string message) : Exception(message) { internal int Error { get; } = error; } /// 容量受限的最大堆:始终只保留“最好的” K 条。键越小越好,堆顶是当前最差的一条。 private sealed class TopK(int capacity) { private readonly long[] _keys = new long[Math.Max(1, capacity)]; private readonly int[] _indexes = new int[Math.Max(1, capacity)]; private int _count; internal void Add(long key, int index) { if (_count < _keys.Length) { int i = _count++; _keys[i] = key; _indexes[i] = index; SiftUp(i); } else if (key < _keys[0]) { _keys[0] = key; _indexes[0] = index; SiftDown(0); } } internal void DrainInto(TopK other) { for (int i = 0; i < _count; i++) other.Add(_keys[i], _indexes[i]); } internal (long Key, int Index)[] ToSortedArray() { var result = new (long, int)[_count]; for (int i = 0; i < _count; i++) result[i] = (_keys[i], _indexes[i]); Array.Sort(result, static (a, b) => a.Item1.CompareTo(b.Item1)); return result; } private void SiftUp(int i) { while (i > 0) { int parent = (i - 1) >> 1; if (_keys[parent] >= _keys[i]) break; Swap(parent, i); i = parent; } } private void SiftDown(int i) { while (true) { int left = (i << 1) + 1; if (left >= _count) return; int largest = left; int right = left + 1; if (right < _count && _keys[right] > _keys[left]) largest = right; if (_keys[i] >= _keys[largest]) return; Swap(i, largest); i = largest; } } private void Swap(int a, int b) { (_keys[a], _keys[b]) = (_keys[b], _keys[a]); (_indexes[a], _indexes[b]) = (_indexes[b], _indexes[a]); } } }