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;
}