using System.ComponentModel; using System.Runtime.InteropServices; using Explorer.Domain.Abstractions; using Microsoft.Extensions.Logging; namespace Explorer.Windows; public sealed class WindowsUsnJournal : IUsnJournal { private readonly ILogger _logger; public WindowsUsnJournal(ILogger logger) => _logger = logger; public bool TryQuery(string rootPath, out UsnJournalState state, out string? error) { state = new UsnJournalState(); error = null; var volume = OpenVolume(rootPath, out error); if (volume == nint.Zero) { return false; } try { var size = Marshal.SizeOf(); var buffer = Marshal.AllocHGlobal(size); try { if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlQueryUsnJournal, 0, 0, buffer, (uint)size, out _, 0)) { error = new Win32Exception(Marshal.GetLastWin32Error()).Message; return false; } var data = Marshal.PtrToStructure(buffer); state = new UsnJournalState { JournalId = unchecked((long)data.UsnJournalId), NextUsn = data.NextUsn }; return true; } finally { Marshal.FreeHGlobal(buffer); } } catch (Exception ex) { error = ex.Message; _logger.LogDebug(ex, "USN query failed for {Path}", rootPath); return false; } finally { NativeMethods.CloseHandle(volume); } } public IReadOnlyList Read(string rootPath, UsnJournalState from, int maxRecords, out UsnJournalState next, out UsnReadStatus status) { next = from; status = UsnReadStatus.Unavailable; var records = new List(); if (!TryQuery(rootPath, out var current, out var error)) { status = error?.Contains("denied", StringComparison.OrdinalIgnoreCase) == true ? UsnReadStatus.AccessDenied : UsnReadStatus.Unavailable; return records; } if (from.JournalId != 0 && from.JournalId != current.JournalId) { status = UsnReadStatus.JournalReset; next = current; return records; } var volume = OpenVolume(rootPath, out _); if (volume == nint.Zero) { status = UsnReadStatus.AccessDenied; return records; } try { var read = new NativeMethods.ReadUsnJournalDataV1 { StartUsn = from.NextUsn == 0 ? current.NextUsn : from.NextUsn, ReasonMask = 0xFFFFFFFF, ReturnOnlyOnClose = 0, Timeout = 0, BytesToWaitFor = 0, UsnJournalId = unchecked((ulong)current.JournalId), MinMajorVersion = 2, MaxMajorVersion = 3 }; var inSize = Marshal.SizeOf(read); var inPtr = Marshal.AllocHGlobal(inSize); var outSize = 64 * 1024; var outPtr = Marshal.AllocHGlobal(outSize); try { Marshal.StructureToPtr(read, inPtr, false); if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlReadUsnJournal, inPtr, (uint)inSize, outPtr, (uint)outSize, out var returned, 0)) { var code = Marshal.GetLastWin32Error(); status = code is 5 or 1314 ? UsnReadStatus.AccessDenied : UsnReadStatus.Error; if (code is 1179 or 1180) { status = UsnReadStatus.JournalReset; } return records; } if (returned < 8) { status = UsnReadStatus.Ok; next = current; return records; } var nextUsn = Marshal.ReadInt64(outPtr); var offset = 8; while (offset + 60 < returned && records.Count < maxRecords) { var recordLength = Marshal.ReadInt32(outPtr, offset); if (recordLength <= 0) { break; } var major = Marshal.ReadInt16(outPtr, offset + 4); var frn = Marshal.ReadInt64(outPtr, offset + 8); var parentFrn = Marshal.ReadInt64(outPtr, offset + 16); var usn = Marshal.ReadInt64(outPtr, offset + 24); var reason = Marshal.ReadInt32(outPtr, offset + 40); var attrs = Marshal.ReadInt32(outPtr, offset + 52); var nameLength = Marshal.ReadInt16(outPtr, offset + 56); var nameOffset = Marshal.ReadInt16(outPtr, offset + 58); var name = Marshal.PtrToStringUni(outPtr + offset + nameOffset, nameLength / 2) ?? ""; records.Add(new UsnRecord { FileReferenceNumber = frn, ParentFileReferenceNumber = parentFrn, Usn = usn, FileName = name, Reason = reason, FileAttributes = attrs }); offset += recordLength; _ = major; } next = new UsnJournalState { JournalId = current.JournalId, NextUsn = nextUsn }; status = UsnReadStatus.Ok; return records; } finally { Marshal.FreeHGlobal(inPtr); Marshal.FreeHGlobal(outPtr); } } catch (Exception ex) { _logger.LogDebug(ex, "USN read failed for {Path}", rootPath); status = UsnReadStatus.Error; return records; } finally { NativeMethods.CloseHandle(volume); } } private static nint OpenVolume(string rootPath, out string? error) { error = null; var letter = Path.GetPathRoot(rootPath)?.TrimEnd('\\'); if (string.IsNullOrEmpty(letter)) { error = "Invalid volume"; return nint.Zero; } var volumePath = @"\\.\" + letter; var handle = NativeMethods.CreateFile( volumePath, NativeMethods.GenericRead, NativeMethods.FileShareRead | NativeMethods.FileShareWrite, 0, NativeMethods.OpenExisting, 0, 0); if (handle == nint.Zero || handle == new nint(-1)) { error = new Win32Exception(Marshal.GetLastWin32Error()).Message; return nint.Zero; } return handle; } }