Files
fluid-explorer/Services/Search/Usn/UsnNative.cs
T

459 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}