using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace FluidExplorer.Services.Search.Usn; /// /// USN 日志 / NTFS 卷所需的最小 P/Invoke 与结构体集合。 /// /// 布局约定:所有 struct 均为 ,默认采用 x64 自然对齐; /// 字段顺序与偏移和 Windows SDK 的 winioctl.h / ntifs.h 完全一致。 /// 每个结构体后面都标注了实测字节大小,便于对照 校验。 /// 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; /// /// 打开 \\.\C: 时用的 dwFlagsAndAttributes。 /// 参考实现(Everything)用 FILE_ATTRIBUTE_READONLY:传 FILE_ATTRIBUTE_NORMAL 在部分环境下会开不了卷句柄。 /// internal const uint FILE_ATTRIBUTE_READONLY = 0x00000001; /// 开目录句柄必须带 FILE_FLAG_BACKUP_SEMANTICS,否则 CreateFile 会失败。 internal const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; /// 顺序扫描提示:读 MFT 时对缓存友好(读一次不再复用)。 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; /// FSCTL_DELETE_USN_JOURNAL 的 DeleteFlags:真正删除日志。 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); // ================================================================ 结构体 /// MFT_ENUM_DATA_V0 —— FSCTL_ENUM_USN_DATA 的输入。x64 大小 24。 [StructLayout(LayoutKind.Sequential)] internal struct MftEnumDataV0 { internal ulong StartFileReferenceNumber; // 0 internal long LowUsn; // 8 枚举时用 0 internal long HighUsn; // 16 枚举时用 long.MaxValue } /// USN_JOURNAL_DATA_V0 —— FSCTL_QUERY_USN_JOURNAL 的输出。x64 大小 56。 [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 } /// READ_USN_JOURNAL_DATA_V0 —— FSCTL_READ_USN_JOURNAL 的输入。x64 大小 40。 [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 } /// /// 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,与磁盘上的紧凑布局严格一致。 /// [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; } /// NTFS_VOLUME_DATA_BUFFER —— FSCTL_GET_NTFS_VOLUME_DATA 的输出。x64 大小 96。 [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 } /// NTFS_FILE_RECORD_INPUT_BUFFER —— FSCTL_GET_NTFS_FILE_RECORD 的输入。x64 大小 8。 [StructLayout(LayoutKind.Sequential)] internal struct NtfsFileRecordInputBuffer { internal ulong FileReferenceNumber; // 只取低 48 位记录号 } /// NTFS_FILE_RECORD_OUTPUT_BUFFER 的固定头。x64 大小 12(后面紧跟变长记录)。 [StructLayout(LayoutKind.Sequential)] internal struct NtfsFileRecordOutputBuffer { internal ulong FileReferenceNumber; // 0 internal uint FileRecordLength; // 8 } /// FSCTL_CREATE_USN_JOURNAL 的输入。x64 大小 16;两个字段都为 0 = 使用系统默认值。 [StructLayout(LayoutKind.Sequential)] internal struct CreateUsnJournalData { internal ulong MaximumSize; // 0 = 系统默认 internal ulong AllocationDelta; // 0 = 系统默认 } /// FSCTL_DELETE_USN_JOURNAL 的输入。x64 大小 16(ulong + DWORD + 对齐填充)。 [StructLayout(LayoutKind.Sequential)] internal struct DeleteUsnJournalData { internal ulong UsnJournalID; internal uint DeleteFlags; } /// /// FILETIME 的精确布局:两个 DWORD。 /// 刻意不用 long —— 在 LayoutKind.Sequential 下 long 会带来 8 字节对齐, /// 从而把 BY_HANDLE_FILE_INFORMATION 的后续字段全部顶偏。 /// [StructLayout(LayoutKind.Sequential)] internal struct FileTimeValue { internal uint LowDateTime; internal uint HighDateTime; internal readonly long ToInt64() => ((long)HighDateTime << 32) | LowDateTime; } /// /// BY_HANDLE_FILE_INFORMATION(GetFileInformationByHandle 的输出)。x64 大小 52。 /// 其中 FileIndexHigh/Low 合起来就是文件的 64 位 FRN(低 48 位记录号 + 高 16 位序列号), /// 与 USN_RECORD_V2 里的 FileReferenceNumber 口径一致 —— 这是 RDCW 回退路径能定位索引条目的关键。 /// [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; } /// FILE_NOTIFY_INFORMATION 的固定头(变长文件名紧跟其后,UTF-16,不以 NUL 结尾)。 [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); /// /// 递归监听目录变化。lpOverlapped == NULL 时是同步阻塞调用(靠 CancelSynchronousIo 打断)。 /// 返回 TRUE 且 bytesReturned == 0 表示变更缓冲区溢出(ERROR_NOTIFY_ENUM_DIR),期间的事件已丢失。 /// [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); // ================================================================ 托管包装 /// 把读/写缓冲固定后调用 DeviceIoControl;返回 false 时用 取错误码。 internal static unsafe bool Ioctl(SafeFileHandle handle, uint code, ReadOnlySpan input, Span 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; } } /// 在卷句柄上做一次带偏移的同步读取(卷句柄偏移 = 卷内绝对字节偏移)。 internal static unsafe int ReadAt(SafeFileHandle handle, Span 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; } } /// /// 按路径取文件的 64 位 FRN 与基本元数据(大小/属性/最后写入时间)。 /// 这是 RDCW 回退监听能定位索引条目的基础:句柄上的 FileIndex 与 USN 的 FRN 同口径。 /// 只要求 FILE_READ_ATTRIBUTES,普通用户也能用(目录需要 FILE_FLAG_BACKUP_SEMANTICS)。 /// 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(); } } /// 把 Win32 错误码翻译成中文可读信息,供 UI 直接展示。 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 "未知错误"; } } // ================================================================ 记录号归一化 /// MFT 记录号掩码:FRN 的低 48 位。 internal const ulong RecordNumberMask = 0x0000_FFFF_FFFF_FFFFUL; /// 剥掉 FRN 高 16 位的序列号,只保留 MFT 记录号(全索引统一用这个做键)。 internal static ulong NormalizeFrn(ulong frn) => frn & RecordNumberMask; }