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