diff --git a/Directory.Build.props b/Directory.Build.props index 44e904b..2f51236 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,11 @@ - 2.0.0 + 2.1.0 latest enable + true + latest-recommended diff --git a/ProjectedFSLib.Managed.Test/DisposeTests.cs b/ProjectedFSLib.Managed.Test/DisposeTests.cs new file mode 100644 index 0000000..412f273 --- /dev/null +++ b/ProjectedFSLib.Managed.Test/DisposeTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Windows.ProjFS; +using NUnit.Framework; +using System; + +namespace ProjectedFSLib.Managed.Test +{ + /// + /// Tests for VirtualizationInstance IDisposable implementation. + /// These tests verify the dispose pattern mechanics without requiring + /// the ProjFS optional feature to be enabled on the machine. + /// + public class DisposeTests + { + [Test] + public void VirtualizationInstance_ImplementsIDisposable() + { + // VirtualizationInstance must implement IDisposable to prevent zombie processes. + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + Assert.That(instance, Is.InstanceOf()); + } + + [Test] + public void IVirtualizationInstance_ExtendsIDisposable() + { + // The interface itself must extend IDisposable so all implementations are required + // to support disposal. + Assert.That(typeof(IDisposable).IsAssignableFrom(typeof(IVirtualizationInstance))); + } + + [Test] + public void Dispose_CanBeCalledMultipleTimes() + { + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + // Should not throw on any call. + instance.Dispose(); + instance.Dispose(); + instance.Dispose(); + } + + [Test] + public void StopVirtualizing_CanBeCalledMultipleTimes() + { + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + // Should not throw on any call. + instance.StopVirtualizing(); + instance.StopVirtualizing(); + } + + [Test] + public void StopVirtualizing_ThenDispose_DoesNotThrow() + { + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + instance.StopVirtualizing(); + instance.Dispose(); + } + + [Test] + public void AfterDispose_MethodsThrowObjectDisposedException() + { + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + instance.Dispose(); + + Assert.Throws(() => + instance.ClearNegativePathCache(out _)); + + Assert.Throws(() => + instance.DeleteFile("test.txt", UpdateType.AllowDirtyMetadata, out _)); + + Assert.Throws(() => + instance.WritePlaceholderInfo( + "test.txt", DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, + System.IO.FileAttributes.Normal, 0, false, new byte[128], new byte[128])); + + Assert.Throws(() => + instance.CreateWriteBuffer(4096)); + + Assert.Throws(() => + instance.CompleteCommand(0)); + + Assert.Throws(() => + instance.StartVirtualizing(null!)); + } + + [Test] + public void AfterStopVirtualizing_MethodsThrowObjectDisposedException() + { + var instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List()); + + instance.StopVirtualizing(); + + // StopVirtualizing should have the same effect as Dispose. + Assert.Throws(() => + instance.ClearNegativePathCache(out _)); + + Assert.Throws(() => + instance.CreateWriteBuffer(4096)); + } + + [Test] + public void UsingStatement_DisposesAutomatically() + { + VirtualizationInstance instance; + using (instance = new VirtualizationInstance( + "C:\\nonexistent", + poolThreadCount: 0, + concurrentThreadCount: 0, + enableNegativePathCache: false, + notificationMappings: new System.Collections.Generic.List())) + { + // Instance is alive here. + } + + // After using block, instance should be disposed. + Assert.Throws(() => + instance.ClearNegativePathCache(out _)); + } + } +} diff --git a/ProjectedFSLib.Managed/ProjFSLib.cs b/ProjectedFSLib.Managed/ProjFSLib.cs index 8da85c8..8002545 100644 --- a/ProjectedFSLib.Managed/ProjFSLib.cs +++ b/ProjectedFSLib.Managed/ProjFSLib.cs @@ -128,7 +128,7 @@ public string NotificationRoot private static void ValidateNotificationRoot(string root) { - if (root == "." || (root != null && root.StartsWith(".\\"))) + if (root == "." || (root != null && root.StartsWith(".\\", StringComparison.Ordinal))) { throw new ArgumentException( "notificationRoot cannot be \".\" or begin with \".\\\""); @@ -216,7 +216,9 @@ public delegate bool NotifyPreCreateHardlinkCallback( // Interfaces public interface IWriteBuffer : IDisposable { +#pragma warning disable CA1720 // Identifier contains type name — established public API, cannot rename IntPtr Pointer { get; } +#pragma warning restore CA1720 UnmanagedMemoryStream Stream { get; } long Length { get; } } @@ -289,7 +291,7 @@ HResult GetFileDataCallback( string triggeringProcessImageFileName); } - public interface IVirtualizationInstance + public interface IVirtualizationInstance : IDisposable { /// Returns the virtualization instance GUID. Guid VirtualizationInstanceId { get; } diff --git a/ProjectedFSLib.Managed/ProjFSNative.cs b/ProjectedFSLib.Managed/ProjFSNative.cs index 3d66d5e..19be1ec 100644 --- a/ProjectedFSLib.Managed/ProjFSNative.cs +++ b/ProjectedFSLib.Managed/ProjFSNative.cs @@ -30,8 +30,11 @@ internal static extern int PrjStartVirtualizing( ref PRJ_CALLBACKS callbacks, IntPtr instanceContext, ref PRJ_STARTVIRTUALIZING_OPTIONS options, - out IntPtr namespaceVirtualizationContext); + out SafeProjFsHandle namespaceVirtualizationContext); + // PrjStopVirtualizing takes raw IntPtr (not SafeProjFsHandle) because it is + // called from SafeProjFsHandle.ReleaseHandle(), where the SafeHandle is already + // closed and cannot be marshaled. All other ProjFS APIs use SafeProjFsHandle. #if NET7_0_OR_GREATER [LibraryImport(ProjFSLib)] internal static partial void PrjStopVirtualizing(IntPtr namespaceVirtualizationContext); @@ -51,7 +54,7 @@ internal static partial int PrjWritePlaceholderInfo( [DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)] internal static extern int PrjWritePlaceholderInfo( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, string destinationFileName, ref PRJ_PLACEHOLDER_INFO placeholderInfo, uint length); @@ -63,7 +66,7 @@ internal static partial int PrjWritePlaceholderInfo2( [DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)] internal static extern int PrjWritePlaceholderInfo2( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, string destinationFileName, ref PRJ_PLACEHOLDER_INFO placeholderInfo, uint placeholderInfoSize, @@ -76,7 +79,7 @@ internal static partial int PrjWritePlaceholderInfo2Raw( [DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "PrjWritePlaceholderInfo2")] internal static extern int PrjWritePlaceholderInfo2Raw( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, IntPtr destinationFileName, IntPtr placeholderInfo, uint placeholderInfoSize, @@ -89,7 +92,7 @@ internal static partial int PrjUpdateFileIfNeeded( [DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)] internal static extern int PrjUpdateFileIfNeeded( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, string destinationFileName, ref PRJ_PLACEHOLDER_INFO placeholderInfo, uint length, @@ -103,7 +106,7 @@ internal static partial int PrjDeleteFile( [DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)] internal static extern int PrjDeleteFile( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, string destinationFileName, uint updateFlags, out uint failureReason); @@ -146,7 +149,7 @@ internal static partial int PrjWriteFileData( [DllImport(ProjFSLib, ExactSpelling = true)] internal static extern int PrjWriteFileData( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, ref Guid dataStreamId, IntPtr buffer, ulong byteOffset, @@ -159,7 +162,7 @@ internal static partial IntPtr PrjAllocateAlignedBuffer( [DllImport(ProjFSLib, ExactSpelling = true)] internal static extern IntPtr PrjAllocateAlignedBuffer( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, UIntPtr size); #if NET7_0_OR_GREATER @@ -181,7 +184,7 @@ internal static partial int PrjCompleteCommand( [DllImport(ProjFSLib, ExactSpelling = true)] internal static extern int PrjCompleteCommand( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, int commandId, int completionResult, IntPtr extendedParameters); @@ -193,7 +196,7 @@ internal static partial int PrjCompleteCommandWithNotification( [DllImport(ProjFSLib, ExactSpelling = true, EntryPoint = "PrjCompleteCommand")] internal static extern int PrjCompleteCommandWithNotification( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, int commandId, int completionResult, ref PRJ_COMPLETE_COMMAND_EXTENDED_PARAMETERS extendedParameters); @@ -209,7 +212,7 @@ internal static partial int PrjClearNegativePathCache( [DllImport(ProjFSLib, ExactSpelling = true)] internal static extern int PrjClearNegativePathCache( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, out uint totalEntryNumber); // ============================ @@ -306,7 +309,7 @@ internal static partial int PrjGetVirtualizationInstanceInfo( [DllImport(ProjFSLib, ExactSpelling = true)] internal static extern int PrjGetVirtualizationInstanceInfo( #endif - IntPtr namespaceVirtualizationContext, + SafeProjFsHandle namespaceVirtualizationContext, ref PRJ_VIRTUALIZATION_INSTANCE_INFO virtualizationInstanceInfo); // ============================ @@ -318,7 +321,7 @@ internal struct PRJ_CALLBACK_DATA { public uint Size; public uint Flags; - public IntPtr NamespaceVirtualizationContext; + public IntPtr namespaceVirtualizationContext; // PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT (native handle, NOT SafeHandle) public int CommandId; public Guid FileId; public Guid DataStreamId; diff --git a/ProjectedFSLib.Managed/SafeProjFsHandle.cs b/ProjectedFSLib.Managed/SafeProjFsHandle.cs new file mode 100644 index 0000000..07e1816 --- /dev/null +++ b/ProjectedFSLib.Managed/SafeProjFsHandle.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Windows.ProjFS +{ + /// + /// SafeHandle wrapper for the PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT returned + /// by PrjStartVirtualizing. Guarantees PrjStopVirtualizing is called even + /// during rude app domain unloads, Environment.Exit, or finalizer-only cleanup. + /// + /// + /// + /// SafeHandle is a CriticalFinalizerObject — the CLR guarantees its + /// ReleaseHandle runs after all normal finalizers and during constrained + /// execution regions. This provides the strongest possible guarantee that + /// the ProjFS virtualization root is released, preventing zombie processes. + /// + /// + internal class SafeProjFsHandle : SafeHandleZeroOrMinusOneIsInvalid + { + /// + /// Parameterless constructor required by P/Invoke marshaler for out-parameter usage. + /// + public SafeProjFsHandle() : base(ownsHandle: true) { } + + protected override bool ReleaseHandle() + { + // Must use the raw 'handle' field (IntPtr) here, not 'this'. + // Inside ReleaseHandle, the SafeHandle is already marked as closed — + // passing 'this' to a P/Invoke taking SafeProjFsHandle would fail + // because the marshaler refuses to marshal a closed SafeHandle. + ProjFSNative.PrjStopVirtualizing(handle); + return true; + } + } +} diff --git a/ProjectedFSLib.Managed/VirtualizationInstance.cs b/ProjectedFSLib.Managed/VirtualizationInstance.cs index 0a47b31..117e0b2 100644 --- a/ProjectedFSLib.Managed/VirtualizationInstance.cs +++ b/ProjectedFSLib.Managed/VirtualizationInstance.cs @@ -24,12 +24,22 @@ public class VirtualizationInstance : IVirtualizationInstance private readonly bool _enableNegativePathCache; private readonly List _notificationMappings; - private IntPtr _context; // PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT + private SafeProjFsHandle _safeContext; // Guarantees PrjStopVirtualizing via critical finalizer private GCHandle _selfHandle; private Guid _instanceId; private IRequiredCallbacks _requiredCallbacks; private GCHandle _notificationMappingsHandle; private IntPtr[] _notificationRootStrings; + private bool _disposed; + + private void ThrowIfDisposed() + { +#if NET8_0_OR_GREATER + ObjectDisposedException.ThrowIf(_disposed, this); +#else + if (_disposed) throw new ObjectDisposedException(nameof(VirtualizationInstance)); +#endif + } // Keep delegates alive to prevent GC while native code holds function pointers private StartDirectoryEnumerationDelegate _startDirEnumDelegate; @@ -54,44 +64,25 @@ public VirtualizationInstance( _enableNegativePathCache = enableNegativePathCache; _notificationMappings = new List(notificationMappings ?? Array.Empty()); - // Match C++/CLI behavior: create the directory and mark as virtualization root - bool markAsRoot = false; + // Create the directory if needed, then always mark as virtualization root. + // PrjMarkDirectoryAsPlaceholder must be called before PrjStartVirtualizing + // to register the directory with the ProjFS driver. Even if the directory + // already has ProjFS state (returns ReparsePointEncountered / 0x8007112B), + // the call primes the driver's in-memory registration — skipping it causes + // PrjStartVirtualizing to fail with ERROR_FILE_SYSTEM_VIRTUALIZATION_PROVIDER_UNKNOWN. var dirInfo = new DirectoryInfo(_rootPath); if (!dirInfo.Exists) { - _instanceId = Guid.NewGuid(); dirInfo.Create(); - markAsRoot = true; - } - else - { - // Check if the directory already has a ProjFS reparse point. - // If not, we need to mark it as a root. - // Use PrjGetOnDiskFileState to detect if it's already a ProjFS placeholder/root. - int hr = ProjFSNative.PrjGetOnDiskFileState(_rootPath, out uint fileState); - if (hr < 0 || fileState == 0) - { - // Not a ProjFS virtualization root yet — need to mark it - _instanceId = Guid.NewGuid(); - markAsRoot = true; - } - else - { - // Already marked. Get the instance ID via PrjGetVirtualizationInstanceInfo - // after StartVirtualizing. For now, generate a new one. - _instanceId = Guid.NewGuid(); - } } - if (markAsRoot) + _instanceId = Guid.NewGuid(); + HResult markResult = MarkDirectoryAsVirtualizationRoot(_rootPath, _instanceId); + if (markResult != HResult.Ok && markResult != HResult.ReparsePointEncountered) { - HResult markResult = MarkDirectoryAsVirtualizationRoot(_rootPath, _instanceId); - if (markResult != HResult.Ok) - { - int errorCode = unchecked((int)markResult) & 0xFFFF; - throw new System.ComponentModel.Win32Exception(errorCode, - $"Failed to mark directory {_rootPath} as virtualization root. HRESULT: 0x{unchecked((uint)markResult):X8}"); - } + int errorCode = unchecked((int)markResult) & 0xFFFF; + throw new System.ComponentModel.Win32Exception(errorCode, + $"Failed to mark directory {_rootPath} as virtualization root. HRESULT: 0x{unchecked((uint)markResult):X8}"); } } @@ -127,6 +118,7 @@ public static HResult MarkDirectoryAsVirtualizationRoot(string rootPath, Guid vi public unsafe HResult StartVirtualizing(IRequiredCallbacks requiredCallbacks) { + ThrowIfDisposed(); _requiredCallbacks = requiredCallbacks ?? throw new ArgumentNullException(nameof(requiredCallbacks)); _selfHandle = GCHandle.Alloc(this); @@ -190,7 +182,7 @@ public unsafe HResult StartVirtualizing(IRequiredCallbacks requiredCallbacks) ref callbacks, GCHandle.ToIntPtr(_selfHandle), ref options, - out _context); + out _safeContext); if (hr < 0) { @@ -213,29 +205,87 @@ public unsafe HResult StartVirtualizing(IRequiredCallbacks requiredCallbacks) // NOTE: Do NOT free allocatedStrings here — ProjFS may cache notification // mapping pointers. They are freed in StopVirtualizing. } + /// + /// Stops the virtualization instance and releases all resources. + /// Equivalent to calling . + /// Retained for backward compatibility — prefer using or . + /// public void StopVirtualizing() { - if (_context != IntPtr.Zero) + Dispose(); + } + + /// + /// Releases all resources used by this VirtualizationInstance. + /// Calls PrjStopVirtualizing if not already stopped, frees GCHandles, + /// and releases unmanaged notification string memory. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged resources. Called by Dispose() and StopVirtualizing(). + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + _disposed = true; + + // Release the ProjFS virtualization context via SafeHandle. + // SafeProjFsHandle.ReleaseHandle calls PrjStopVirtualizing. + // Even if this Dispose is skipped, the SafeHandle's critical + // finalizer guarantees cleanup. + if (_safeContext != null && !_safeContext.IsInvalid && !_safeContext.IsClosed) { - ProjFSNative.PrjStopVirtualizing(_context); - _context = IntPtr.Zero; + _safeContext.Dispose(); } if (_selfHandle.IsAllocated) { _selfHandle.Free(); } + + // Free notification mapping strings allocated via Marshal.StringToHGlobalUni + if (_notificationRootStrings != null) + { + foreach (var ptr in _notificationRootStrings) + { + if (ptr != IntPtr.Zero) + Marshal.FreeHGlobal(ptr); + } + _notificationRootStrings = null; + } + + if (_notificationMappingsHandle.IsAllocated) + { + _notificationMappingsHandle.Free(); + } + } + + /// + /// Destructor ensures PrjStopVirtualizing is called if Dispose was not. + /// Prevents zombie processes when the VirtualizationInstance is abandoned. + /// + ~VirtualizationInstance() + { + Dispose(false); } public HResult ClearNegativePathCache(out uint totalEntryNumber) { - int hr = ProjFSNative.PrjClearNegativePathCache(_context, out totalEntryNumber); + ThrowIfDisposed(); + int hr = ProjFSNative.PrjClearNegativePathCache(_safeContext, out totalEntryNumber); return (HResult)hr; } public HResult DeleteFile(string relativePath, UpdateType updateFlags, out UpdateFailureCause failureReason) { - int hr = ProjFSNative.PrjDeleteFile(_context, relativePath, (uint)updateFlags, out uint cause); + ThrowIfDisposed(); + int hr = ProjFSNative.PrjDeleteFile(_safeContext, relativePath, (uint)updateFlags, out uint cause); failureReason = (UpdateFailureCause)cause; return (HResult)hr; } @@ -252,6 +302,7 @@ public unsafe HResult WritePlaceholderInfo( byte[] contentId, byte[] providerId) { + ThrowIfDisposed(); var info = new PRJ_PLACEHOLDER_INFO(); info.FileBasicInfo.IsDirectory = isDirectory ? (byte)1 : (byte)0; info.FileBasicInfo.FileSize = endOfFile; @@ -264,7 +315,7 @@ public unsafe HResult WritePlaceholderInfo( CopyIdToVersionInfo(contentId, providerId, ref info.VersionInfo); int hr = ProjFSNative.PrjWritePlaceholderInfo( - _context, + _safeContext, relativePath, ref info, (uint)Marshal.SizeOf()); @@ -299,6 +350,7 @@ public unsafe HResult WritePlaceholderInfo2( byte[] contentId, byte[] providerId) { + ThrowIfDisposed(); var info = new PRJ_PLACEHOLDER_INFO(); info.FileBasicInfo.IsDirectory = isDirectory ? (byte)1 : (byte)0; info.FileBasicInfo.FileSize = isDirectory ? 0 : endOfFile; @@ -326,7 +378,7 @@ public unsafe HResult WritePlaceholderInfo2( PRJ_EXTENDED_INFO* pExt = &extendedInfo; hr = ProjFSNative.PrjWritePlaceholderInfo2Raw( - _context, + _safeContext, (IntPtr)pPath, (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer(ref info), (uint)sizeof(PRJ_PLACEHOLDER_INFO), @@ -338,7 +390,7 @@ public unsafe HResult WritePlaceholderInfo2( else { int hr = ProjFSNative.PrjWritePlaceholderInfo( - _context, + _safeContext, relativePath, ref info, (uint)Marshal.SizeOf()); @@ -359,6 +411,7 @@ public unsafe HResult UpdateFileIfNeeded( UpdateType updateFlags, out UpdateFailureCause failureReason) { + ThrowIfDisposed(); var info = new PRJ_PLACEHOLDER_INFO(); info.FileBasicInfo.IsDirectory = 0; info.FileBasicInfo.FileSize = endOfFile; @@ -371,7 +424,7 @@ public unsafe HResult UpdateFileIfNeeded( CopyIdToVersionInfo(contentId, providerId, ref info.VersionInfo); int hr = ProjFSNative.PrjUpdateFileIfNeeded( - _context, + _safeContext, relativePath, ref info, (uint)Marshal.SizeOf(), @@ -384,24 +437,27 @@ public unsafe HResult UpdateFileIfNeeded( public HResult CompleteCommand(int commandId, HResult completionResult) { - int hr = ProjFSNative.PrjCompleteCommand(_context, commandId, (int)completionResult, IntPtr.Zero); + ThrowIfDisposed(); + int hr = ProjFSNative.PrjCompleteCommand(_safeContext, commandId, (int)completionResult, IntPtr.Zero); return (HResult)hr; } public HResult CompleteCommand(int commandId, NotificationType newNotificationMask) { + ThrowIfDisposed(); var extParams = new PRJ_COMPLETE_COMMAND_EXTENDED_PARAMETERS { CommandType = PRJ_COMPLETE_COMMAND_TYPE_NOTIFICATION, NotificationMask = (uint)newNotificationMask, }; - int hr = ProjFSNative.PrjCompleteCommandWithNotification(_context, commandId, 0, ref extParams); + int hr = ProjFSNative.PrjCompleteCommandWithNotification(_safeContext, commandId, 0, ref extParams); return (HResult)hr; } public HResult CompleteCommand(int commandId, IDirectoryEnumerationResults results) { + ThrowIfDisposed(); var dirResults = (DirectoryEnumerationResults)results; var extParams = new PRJ_COMPLETE_COMMAND_EXTENDED_PARAMETERS { @@ -409,27 +465,30 @@ public HResult CompleteCommand(int commandId, IDirectoryEnumerationResults resul DirEntryBufferHandle = dirResults.DirEntryBufferHandle, }; - int hr = ProjFSNative.PrjCompleteCommandWithNotification(_context, commandId, 0, ref extParams); + int hr = ProjFSNative.PrjCompleteCommandWithNotification(_safeContext, commandId, 0, ref extParams); return (HResult)hr; } public HResult CompleteCommand(int commandId) { - int hr = ProjFSNative.PrjCompleteCommand(_context, commandId, 0, IntPtr.Zero); + ThrowIfDisposed(); + int hr = ProjFSNative.PrjCompleteCommand(_safeContext, commandId, 0, IntPtr.Zero); return (HResult)hr; } public IWriteBuffer CreateWriteBuffer(uint desiredBufferSize) { - return new WriteBuffer(_context, desiredBufferSize); + ThrowIfDisposed(); + return new WriteBuffer(_safeContext, desiredBufferSize); } public IWriteBuffer CreateWriteBuffer(ulong byteOffset, uint length, out ulong alignedByteOffset, out uint alignedLength) { + ThrowIfDisposed(); // Get the sector size from PrjGetVirtualizationInstanceInfo so we can // compute aligned values for byteOffset and length. var instanceInfo = new PRJ_VIRTUALIZATION_INSTANCE_INFO(); - int hr = ProjFSNative.PrjGetVirtualizationInstanceInfo(_context, ref instanceInfo); + int hr = ProjFSNative.PrjGetVirtualizationInstanceInfo(_safeContext, ref instanceInfo); if (hr < 0) { throw new System.ComponentModel.Win32Exception(hr, @@ -453,12 +512,14 @@ public IWriteBuffer CreateWriteBuffer(ulong byteOffset, uint length, out ulong a public HResult WriteFileData(Guid dataStreamId, IWriteBuffer buffer, ulong byteOffset, uint length) { - int hr = ProjFSNative.PrjWriteFileData(_context, ref dataStreamId, buffer.Pointer, byteOffset, length); + ThrowIfDisposed(); + int hr = ProjFSNative.PrjWriteFileData(_safeContext, ref dataStreamId, buffer.Pointer, byteOffset, length); return (HResult)hr; } public unsafe HResult MarkDirectoryAsPlaceholder(string targetDirectoryPath, byte[] contentId, byte[] providerId) { + ThrowIfDisposed(); var versionInfo = new PRJ_PLACEHOLDER_VERSION_INFO(); CopyIdToVersionInfo(contentId, providerId, ref versionInfo); diff --git a/ProjectedFSLib.Managed/WriteBuffer.cs b/ProjectedFSLib.Managed/WriteBuffer.cs index e24d6c1..e2daa7a 100644 --- a/ProjectedFSLib.Managed/WriteBuffer.cs +++ b/ProjectedFSLib.Managed/WriteBuffer.cs @@ -14,12 +14,12 @@ public class WriteBuffer : IWriteBuffer private IntPtr _buffer; private bool _disposed; - internal unsafe WriteBuffer(IntPtr virtualizationContext, uint desiredBufferSize) + internal unsafe WriteBuffer(SafeProjFsHandle virtualizationContext, uint desiredBufferSize) { _buffer = ProjFSNative.PrjAllocateAlignedBuffer(virtualizationContext, new UIntPtr(desiredBufferSize)); if (_buffer == IntPtr.Zero) { - throw new OutOfMemoryException("PrjAllocateAlignedBuffer returned null"); + throw new InvalidOperationException("PrjAllocateAlignedBuffer returned null — insufficient memory or invalid context."); } Length = desiredBufferSize; @@ -32,7 +32,9 @@ internal unsafe WriteBuffer(IntPtr virtualizationContext, uint desiredBufferSize Dispose(false); } +#pragma warning disable CA1720 // Identifier contains type name — established public API public IntPtr Pointer => _buffer; +#pragma warning restore CA1720 public UnmanagedMemoryStream Stream { get; private set; }