global using System.Threading; global using System.Threading.Tasks; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.AccessControl; using Vanara.PInvoke; using static Vanara.PInvoke.AdvApi32; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.VirtDisk; namespace Vanara.IO; /// Class that represents a virtual disk and allows for performing actions on it. This wraps most of the methods found in virtdisk.h. /// [DebuggerDisplay("[{ImagePath} - Attached={Attached}]")] public partial class VirtualDisk : IDisposable, IHandle { private static readonly bool IsPreWin8 = !PInvokeClient.Windows8.IsPlatformSupported(); private readonly OPEN_VIRTUAL_DISK_VERSION ver; private VirtualDiskMetadata? metadata; private VirtualDisk(SafeVIRTUAL_DISK_HANDLE handle, OPEN_VIRTUAL_DISK_VERSION version, string imgPath) { if (handle is null || handle.IsInvalid) { throw new ArgumentNullException(nameof(handle)); } Handle = handle; ImagePath = imgPath; ver = version; } private delegate Win32Error RunAsyncMethod(in NativeOverlapped overlap); /// Represents the format of the virtual disk. public enum DeviceType : uint { /// Device type is unknown or not valid. Unknown = VIRTUAL_STORAGE_TYPE_DEVICE_TYPE.VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN, /// /// CD or DVD image file device type. (.iso file) /// Windows 7 and Windows Server 2008 R2: This value is not supported before Windows 8 and Windows Server 2012. /// Iso = VIRTUAL_STORAGE_TYPE_DEVICE_TYPE.VIRTUAL_STORAGE_TYPE_DEVICE_ISO, /// Virtual hard disk device type. (.vhd file) Vhd = VIRTUAL_STORAGE_TYPE_DEVICE_TYPE.VIRTUAL_STORAGE_TYPE_DEVICE_VHD, /// /// VHDX format virtual hard disk device type. (.vhdx file) /// Windows 7 and Windows Server 2008 R2: This value is not supported before Windows 8 and Windows Server 2012. /// Vhdx = VIRTUAL_STORAGE_TYPE_DEVICE_TYPE.VIRTUAL_STORAGE_TYPE_DEVICE_VHDX, /// /// VHD Set files (.vhds file) are a new shared Virtual Disk model for guest clusters in Windows Server 2016. VHD Set files support /// online resizing of shared virtual disks, support Hyper-V Replica, and can be included in application-consistent checkpoints. /// VhdSet = VIRTUAL_STORAGE_TYPE_DEVICE_TYPE.VIRTUAL_STORAGE_TYPE_DEVICE_VHDSET } /// Represents the subtype of a virtual disk. public enum Subtype : uint { /// Fixed. Fixed = 2, /// Dynamically expandable (sparse). Dynamic = 3, /// Differencing. Differencing = 4 } /// Indicates whether this virtual disk is currently attached. public bool Attached => IsAttached(ImagePath); /// Block size of the VHD, in bytes. public uint BlockSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_SIZE, 16); /// The device identifier. public DeviceType DiskType => (DeviceType)GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE); /// The fragmentation level of the virtual disk. public uint? FragmentationPercentage => IsChild ? null : GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_FRAGMENTATION); /// Unique identifier of the VHD. public Guid Identifier { get => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_IDENTIFIER); set { SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_IDENTIFIER) { UniqueIdentifier = value }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } } /// Gets the path of the image file provided when opening or creating this instance. /// The image path. public string ImagePath { get; private set; } /// Indicates whether the virtual disk is 4 KB aligned. public bool Is4kAligned => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_IS_4K_ALIGNED); /// Gets a value indicating whether this virtual disk has a parent backing store. /// if this instance has a parent backing store; otherwise, . public bool IsChild => ProviderSubtype == Subtype.Differencing; /// /// Indicates whether the virtual disk is currently mounted and in use. TRUE if the virtual disk is currently mounted and in use; /// otherwise FALSE. /// public bool IsLoaded => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_IS_LOADED); /// Indicates whether the physical disk is remote. public bool IsRemote => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PHYSICAL_DISK, 8); /// The logical sector size of the physical disk. public uint LogicalSectorSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PHYSICAL_DISK); /// Gets the metadata associated with this virtual disk. Currently on VHDX files support metadata. /// The metadata. public VirtualDiskMetadata Metadata => metadata ??= new VirtualDiskMetadata(this); /// /// The change tracking identifier for the change that identifies the state of the virtual disk that you want to use as the basis of /// comparison to determine whether the NewerChanges member reports new changes. /// public string MostRecentId => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_CHANGE_TRACKING_STATE, 8); /// /// Whether the virtual disk has changed since the change identified by the MostRecentId member occurred. TRUE if the virtual disk has /// changed since the change identified by the MostRecentId member occurred; otherwise FALSE. /// public bool NewerChanges => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_CHANGE_TRACKING_STATE, 4); /// The path of the parent backing store, if it can be resolved. public string? ParentBackingStore => IsChild ? GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PARENT_LOCATION, 4) : null; /// Unique identifier of the parent disk backing store. If unattached, is returned. public Guid? ParentIdentifier => IsChild ? GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PARENT_IDENTIFIER) : null; /// /// If this is a child, contains the path of the parent backing store. If is a parent disk, contains all of the parent paths present in /// the search list. If unattached, is returned. /// public string[]? ParentPaths { get { if (IsChild) { var pPath = GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PARENT_LOCATION, 4); return !string.IsNullOrEmpty(pPath) ? new[] { pPath } : null; } if (Attached) { return GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PARENT_LOCATION, 4); } return null; } } /// Internal time stamp of the parent disk backing store. If unattached, is returned. public uint? ParentTimeStamp => IsChild ? GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PARENT_TIMESTAMP) : null; /// /// Retrieves the path to the physical device object that contains a virtual hard disk (VHD) or CD or DVD image file (ISO). If /// unattached, is returned. /// public string? PhysicalPath { get { if (!Attached) return null; var sz = 64; StringBuilder sb = new(sz); Win32Error err; do { err = GetVirtualDiskPhysicalPath(Handle, ref sz, sb); if (err == Win32Error.ERROR_INSUFFICIENT_BUFFER) sb.Capacity *= 4; } while (err == Win32Error.ERROR_INSUFFICIENT_BUFFER); err.ThrowIfFailed(); return sb.ToString(); } } /// The physical sector size of the physical disk. public uint PhysicalSectorSize { get => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PHYSICAL_DISK, 4); set { SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_PHYSICAL_SECTOR_SIZE) { VhdPhysicalSectorSize = value }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } } /// Physical size of the VHD on disk, in bytes. public ulong PhysicalSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_SIZE, 8); /// Provider-specific subtype. public Subtype ProviderSubtype => (Subtype)GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE); /// Whether RCT is turned on. TRUE if RCT is turned on; otherwise FALSE. public bool ResilientChangeTrackingEnabled { get => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_CHANGE_TRACKING_STATE); set { SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_CHANGE_TRACKING_STATE) { ChangeTrackingEnabled = value }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } } /// Sector size of the VHD, in bytes. public uint SectorSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_SIZE, 20); /// The smallest safe minimum size of the virtual disk. If unattached, is returned. public ulong? SmallestSafeVirtualSize => Attached ? GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_SMALLEST_SAFE_VIRTUAL_SIZE) : null; /// Vendor-unique identifier. public Guid VendorId => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE, 4); /// The physical sector size of the virtual disk. public uint VhdPhysicalSectorSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_VHD_PHYSICAL_SECTOR_SIZE); /// /// The identifier that is uniquely created when a user first creates the virtual disk to attempt to uniquely identify that virtual disk. /// public Guid VirtualDiskId { get => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_VIRTUAL_DISK_ID); set { SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_VIRTUAL_DISK_ID) { VirtualDiskId = value }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } } /// Virtual size of the VHD, in bytes. public ulong VirtualSize => GetInformation(GET_VIRTUAL_DISK_INFO_VERSION.GET_VIRTUAL_DISK_INFO_SIZE); /// Gets the volume GUID paths for an attached virtual disk. public string[]? VolumeGuidPaths { get { if (!Attached || PhysicalPath is null) return null; var diskNo = GetDiskNumberFromDevicePath(PhysicalPath); return diskNo.HasValue ? GetVolumeGuidsFromDiskNumber(diskNo.Value).ToArray() : null; } } /// Gets the volume mount points for an attached virtual disk. public string[]? VolumeMountPoints { get { var volPath = VolumeGuidPaths?.FirstOrDefault(); return volPath is null ? null : GetVolumeMountPoints(volPath); } } /// Gets the safe handle for the current virtual disk. private SafeVIRTUAL_DISK_HANDLE Handle { get; set; } /// Creates a virtual hard disk (VHD) image file. /// A valid file path that represents the path to the new virtual disk image file. /// A reference to a valid CREATE_VIRTUAL_DISK_PARAMETERS structure that contains creation parameter data. /// Creation flags, which must be a valid combination of the CREATE_VIRTUAL_DISK_FLAG enumeration. /// The VIRTUAL_DISK_ACCESS_MASK value to use when opening the newly created virtual disk file. /// /// An optional pointer to a SECURITY_DESCRIPTOR to apply to the virtual disk image file. If this parameter is IntPtr.Zero, the parent /// directory's security descriptor will be used. /// /// If successful, returns a valid instance for the newly created virtual disk. public static VirtualDisk Create(string path, in CREATE_VIRTUAL_DISK_PARAMETERS param, CREATE_VIRTUAL_DISK_FLAG flags = CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE, VIRTUAL_DISK_ACCESS_MASK mask = VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE, PSECURITY_DESCRIPTOR securityDescriptor = default) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } VIRTUAL_STORAGE_TYPE stType = new(); CreateVirtualDisk(stType, path, mask, securityDescriptor, flags, 0, param, IntPtr.Zero, out SafeVIRTUAL_DISK_HANDLE handle).ThrowIfFailed(); return new VirtualDisk(handle, (OPEN_VIRTUAL_DISK_VERSION)param.Version, path); } /// Creates a virtual hard disk (VHD) image file, either using default parameters or using an existing VHD or physical disk. /// A valid file path that represents the path to the new virtual disk image file. /// The maximum virtual size, in bytes, of the virtual disk object. Must be a multiple of 512. /// /// true to grow the disk dynamically as content is added; false to pre-allocate all physical space necessary for the size /// of the virtual disk. /// /// /// An optional FileSecurity instance to apply to the attached virtual disk. If this parameter is null, the security descriptor of /// the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to the attached virtual disk /// grants the write attributes permission for the user, or that the security descriptor of the virtual disk image file grants the write /// attributes permission for the user if you specify null for this parameter. If the security descriptor does not grant write /// attributes permission for a user, Shell displays the following error when the user accesses the attached virtual disk: The Recycle /// Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// /// If successful, returns a valid instance for the newly created virtual disk. public static VirtualDisk Create(string path, ulong size, bool dynamic = true, FileSecurity? access = null) => Create(path, size, dynamic, 0, 0, access); /// Creates a virtual hard disk (VHD) image file, either using default parameters or using an existing VHD or physical disk. /// A valid file path that represents the path to the new virtual disk image file. /// The maximum virtual size, in bytes, of the virtual disk object. Must be a multiple of 512. /// /// true to grow the disk dynamically as content is added; false to pre-allocate all physical space necessary for the size /// of the virtual disk. /// /// /// Internal size of the virtual disk object blocks, in bytes. For VHDX this must be a multiple of 1 MB between 1 and 256 MB. For VHD 1 /// this must be set to one of the following values: 0 (default), 0x80000 (512K), or 0x200000 (2MB) /// /// /// Internal size of the virtual disk object sectors. For VHDX must be set to 512 (0x200) or 4096 (0x1000). For VHD 1 must be set to 512. /// /// /// An optional FileSecurity instance to apply to the attached virtual disk. If this parameter is null, the security descriptor of /// the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to the attached virtual disk /// grants the write attributes permission for the user, or that the security descriptor of the virtual disk image file grants the write /// attributes permission for the user if you specify null for this parameter. If the security descriptor does not grant write /// attributes permission for a user, Shell displays the following error when the user accesses the attached virtual disk: The Recycle /// Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// /// If successful, returns a valid instance for the newly created virtual disk. public static VirtualDisk Create(string path, ulong size, bool dynamic, uint blockSize = 0, uint logicalSectorSize = 0, FileSecurity? access = null) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } VIRTUAL_DISK_ACCESS_MASK mask = IsPreWin8 ? VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_CREATE : VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE; SafePSECURITY_DESCRIPTOR sd = FileSecToSd(access); CREATE_VIRTUAL_DISK_PARAMETERS param = new(size, IsPreWin8 ? 1U : 2U, blockSize, logicalSectorSize); CREATE_VIRTUAL_DISK_FLAG flags = dynamic ? CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE : CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION; return Create(path, param, flags, mask, sd); } /// Creates a virtual hard disk (VHD) image file. /// A valid file path that represents the path to the new virtual disk image file. /// A reference to a valid CREATE_VIRTUAL_DISK_PARAMETERS structure that contains creation parameter data. /// Creation flags, which must be a valid combination of the CREATE_VIRTUAL_DISK_FLAG enumeration. /// The VIRTUAL_DISK_ACCESS_MASK value to use when opening the newly created virtual disk file. /// A VIRTUAL_STORAGE_TYPE structure that contains the desired disk type and vendor information. /// /// An optional pointer to a SECURITY_DESCRIPTOR to apply to the virtual disk image file. If this parameter is IntPtr.Zero, the parent /// directory's security descriptor will be used. /// /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// If successful, returns a valid instance for the newly created virtual disk. public static async Task CreateAsync(string path, CREATE_VIRTUAL_DISK_PARAMETERS param, CREATE_VIRTUAL_DISK_FLAG flags = CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE, VIRTUAL_DISK_ACCESS_MASK mask = VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE, VIRTUAL_STORAGE_TYPE storageType = default, PSECURITY_DESCRIPTOR securityDescriptor = default, CancellationToken cancellationToken = default, IProgress? progress = default) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } return await Task.Run(() => { NativeOverlapped vhdOverlap = new(); CreateVirtualDisk(storageType, path, mask, securityDescriptor, flags, 0, param, vhdOverlap, out SafeVIRTUAL_DISK_HANDLE hVhd).ThrowUnless(Win32Error.ERROR_IO_PENDING); if (!GetProgress(hVhd, vhdOverlap, cancellationToken, progress)) throw new OperationCanceledException(cancellationToken); return new VirtualDisk(hVhd, (OPEN_VIRTUAL_DISK_VERSION)param.Version, path); }); } /// Creates a virtual hard disk (VHD) image file, either using default parameters or using an existing VHD or physical disk. /// A valid string that represents the path to the new virtual disk image file. /// /// /// An optional pointer to a FileSecurity instance to apply to the attached virtual disk. If this parameter is NULL, the security /// descriptor of the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to the attached /// virtual disk grants the write attributes permission for the user, or that the security descriptor of the virtual disk image file /// grants the write attributes permission for the user if you specify NULL for this parameter.If the security descriptor does not grant /// write attributes permission for a user, Shell displays the following error when the user accesses the attached virtual disk: The /// Recycle Bin is corrupted.Do you want to empty the Recycle Bin for this drive? /// /// public static VirtualDisk CreateDifferencing(string path, string parentPath, FileSecurity? access = null) { //if (access == null) access = GetFileSecurity(); if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } if (string.IsNullOrEmpty(parentPath)) { throw new ArgumentNullException(nameof(parentPath)); } // If this is V2 (>=Win8), then let the file extension determine type, otherwise, it has to be a VHD VIRTUAL_DISK_ACCESS_MASK mask = IsPreWin8 ? VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_CREATE : VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE; SafePSECURITY_DESCRIPTOR sd = FileSecToSd(access); using SafeCoTaskMemString pp = new(parentPath); CREATE_VIRTUAL_DISK_PARAMETERS param = new(pp, default); return Create(path, param, CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE, mask, sd); } /// Creates a virtual hard disk (VHD) image file, either using default parameters or using an existing VHD or physical disk. /// A valid file path that represents the path to the new virtual disk image file. /// /// A fully qualified path to pre-populate the new virtual disk object with block data from an existing disk. This path may refer to a /// virtual disk or a physical disk. /// /// If successful, returns a valid instance for the newly created virtual disk. public static VirtualDisk CreateFromSource(string path, string sourcePath) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } if (string.IsNullOrEmpty(sourcePath)) { throw new ArgumentNullException(nameof(sourcePath)); } VIRTUAL_DISK_ACCESS_MASK mask = IsPreWin8 ? VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_CREATE : VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE; using SafeCoTaskMemString sp = new(sourcePath); CREATE_VIRTUAL_DISK_PARAMETERS param = new(default, sp); return Create(path, param, CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE, mask); } /// Creates a virtual hard disk (VHD) image file, either using default parameters or using an existing VHD or physical disk. /// A valid file path that represents the path to the new virtual disk image file. /// /// A fully qualified path to pre-populate the new virtual disk object with block data from an existing disk. This path may refer to a /// virtual disk or a physical disk. /// /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// If successful, returns a valid instance for the newly created virtual disk. public static async Task CreateFromSourceAsync(string path, string sourcePath, CancellationToken cancellationToken = default, IProgress? progress = default) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } if (string.IsNullOrEmpty(sourcePath)) { throw new ArgumentNullException(nameof(sourcePath)); } VIRTUAL_STORAGE_TYPE stType = new(); VIRTUAL_DISK_ACCESS_MASK mask = IsPreWin8 ? VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_CREATE : VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE; using SafeCoTaskMemString sp = new(sourcePath); CREATE_VIRTUAL_DISK_PARAMETERS param = new(default, sp); CREATE_VIRTUAL_DISK_FLAG flags = CREATE_VIRTUAL_DISK_FLAG.CREATE_VIRTUAL_DISK_FLAG_NONE; return await CreateAsync(path, param, flags, mask, stType, default, cancellationToken, progress); } /// /// Detach a virtual disk that was previously attached with the ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME flag or calling and setting autoDetach to false. /// /// A valid path to the virtual disk image to detach. public static void Detach(string path) { try { using VirtualDisk vd = Open(path, OPEN_VIRTUAL_DISK_FLAG.OPEN_VIRTUAL_DISK_FLAG_NONE, null, VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_DETACH); vd.Detach(); } catch { } } /// Gets the list of all the loopback mounted virtual disks. /// An enumeration of all the loopback mounted virtual disks physical paths. public static IEnumerable GetAllAttachedVirtualDiskPaths() { uint sz = 0; SafeCoTaskMemHandle sb = new(0); Win32Error err; do { err = GetAllAttachedVirtualDiskPhysicalPaths(ref sz, sb); if (err.Succeeded) { break; } if (err != Win32Error.ERROR_INSUFFICIENT_BUFFER) { err.ThrowIfFailed(); } sb.Size = (int)sz; } while (err == Win32Error.ERROR_INSUFFICIENT_BUFFER); return sb.Size <= 1 ? new string[0] : sb.ToStringEnum(CharSet.Unicode); } /// /// Returns the relationships between virtual hard disks (VHDs) or CD or DVD image file (ISO) or the volumes contained within those disks /// and their parent disk or volume. /// /// /// /// A handle to a volume or root directory if the Flags parameter does not specify the /// GET_STORAGE_DEPENDENCY_FLAG_DISK_HANDLE flag. For information on how to open a volume or root directory, see the CreateFile function. /// /// /// If the Flags parameter specifies the GET_STORAGE_DEPENDENCY_FLAG_DISK_HANDLE flag, this handle should be a handle to a disk. /// /// /// A valid combination of GET_STORAGE_DEPENDENCY_FLAG values. /// An array of structures with the requested information. /// CD and DVD image files (ISO) are not supported before Windows 8 and Windows Server 2012. public static STORAGE_DEPENDENCY_INFO_TYPE_2[] GetStorageDependencyInformation(IntPtr handle, GET_STORAGE_DEPENDENCY_FLAG flags) { using var mem = SafeCoTaskMemHandle.CreateFromStructure(new STORAGE_DEPENDENCY_INFO { Version = STORAGE_DEPENDENCY_INFO_VERSION.STORAGE_DEPENDENCY_INFO_VERSION_2 }); Win32Error err = VirtDisk.GetStorageDependencyInformation(handle, flags, mem.Size, mem, out var sz); if (err == Win32Error.ERROR_INSUFFICIENT_BUFFER) { mem.Size = sz; err = VirtDisk.GetStorageDependencyInformation(handle, flags, mem.Size, mem, out sz); } err.ThrowIfFailed(); // Get array at offset 8 from count at offset 4 return mem.ToArray(mem.ToStructure(sizeof(int)), sizeof(uint) * 2); } /// Determines whether the specified virtual disk indicated by is currently attached. /// The path to the virtual disk image file. /// if the specified path is attached; otherwise, . public static bool IsAttached(string path) => GetAllAttachedVirtualDiskPaths().Any(s => s.Equals(path, StringComparison.InvariantCultureIgnoreCase)); /// Creates an instance of a Virtual Disk from a file. /// A valid path to the virtual disk image to open. /// A valid combination of values of the OPEN_VIRTUAL_DISK_FLAG enumeration. /// A valid OPEN_VIRTUAL_DISK_PARAMETERS structure. /// A valid VIRTUAL_DISK_ACCESS_MASK value. public static VirtualDisk Open(string path, OPEN_VIRTUAL_DISK_FLAG flags = OPEN_VIRTUAL_DISK_FLAG.OPEN_VIRTUAL_DISK_FLAG_NONE, OPEN_VIRTUAL_DISK_PARAMETERS? param = null, VIRTUAL_DISK_ACCESS_MASK mask = VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } Debug.WriteLine($"OpenVD: mask={mask}; flags={flags}; param={param}"); OpenVirtualDisk(new VIRTUAL_STORAGE_TYPE(), path, mask, flags, param, out SafeVIRTUAL_DISK_HANDLE hVhd).ThrowIfFailed(); return new VirtualDisk(hVhd, param?.Version ?? (IsPreWin8 ? OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_1 : OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2), path); } /// Creates an instance of a Virtual Disk from a file. /// A valid path to the virtual disk image to open. /// If TRUE, indicates the file backing store is to be opened as read-only. /// If TRUE, indicates the handle is only to be used to get information on the virtual disk. /// /// Open the VHD file (backing store) without opening any differencing-chain parents. Used to correct broken parent links. This flag is /// not supported for ISO virtual disks. /// public static VirtualDisk Open(string path, bool readOnly, bool getInfoOnly = false, bool noParents = false) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); OPEN_VIRTUAL_DISK_FLAG flags = OPEN_VIRTUAL_DISK_FLAG.OPEN_VIRTUAL_DISK_FLAG_NONE; if (noParents) flags |= OPEN_VIRTUAL_DISK_FLAG.OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS; var isIso = Path.GetExtension(path).Equals(".iso", StringComparison.InvariantCultureIgnoreCase); if (isIso && (!readOnly || noParents)) throw new NotSupportedException(); OPEN_VIRTUAL_DISK_PARAMETERS param; VIRTUAL_DISK_ACCESS_MASK mask = VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_NONE; if (isIso || IsPreWin8) { param = new OPEN_VIRTUAL_DISK_PARAMETERS(0); // make v1 instance if (readOnly) mask |= VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_READ; if (getInfoOnly) mask |= VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_GET_INFO; } else { param = new OPEN_VIRTUAL_DISK_PARAMETERS(readOnly, getInfoOnly); } return Open(path, flags, param, mask); } /// Attaches a parent to a virtual disk opened with the OPEN_VIRTUAL_DISK_FLAG_CUSTOM_DIFF_CHAIN flag. /// A valid path to the virtual hard disk image to add as a parent. /// /// This adds the specified parent virtual hard disk to the head of the differencing chain of the specified virtual hard disk. If the /// differencing chain extends beyond the parent, this function can be called repeatedly to add additional parents to the differencing chain. /// public void AddParent(string parentPath) => AddVirtualDiskParent(Handle, parentPath).ThrowIfFailed(); /// Applies a snapshot of the current virtual disk for VHD Set files. /// The ID of the new snapshot to be applied to the VHD set. /// /// Indicates whether the current default leaf data should be retained as part of the apply operation. When is /// specified, the apply operation will discard the current default leaf data. When a non-zero GUID is specified, the apply operation /// will convert the default leaf data into a writeable snapshot with the specified ID. /// /// to indicate that the snapshot to be applied was created as a writable snapshot type. public void ApplySnapshot(Guid id, Guid leafId = default, bool writeable = false) { APPLY_SNAPSHOT_VHDSET_PARAMETERS param = new() { Version = APPLY_SNAPSHOT_VHDSET_VERSION.APPLY_SNAPSHOT_VHDSET_VERSION_1, Version1 = new() { SnapshotId = id, LeafSnapshotId = leafId } }; ApplySnapshotVhdSet(Handle, param, writeable ? 0 : APPLY_SNAPSHOT_VHDSET_FLAG.APPLY_SNAPSHOT_VHDSET_FLAG_WRITEABLE).ThrowIfFailed(); } /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// A valid combination of values of the ATTACH_VIRTUAL_DISK_FLAG enumeration. /// A reference to a valid ATTACH_VIRTUAL_DISK_PARAMETERS structure that contains attachment parameter data. /// /// An optional pointer to a SECURITY_DESCRIPTOR to apply to the attached virtual disk. If this parameter is NULL, the security /// descriptor of the virtual disk image file is used. /// /// Ensure that the security descriptor that AttachVirtualDisk applies to the attached virtual disk grants the write attributes /// permission for the user, or that the security descriptor of the virtual disk image file grants the write attributes permission for /// the user if you specify NULL for this parameter. If the security descriptor does not grant write attributes permission for a user, /// Shell displays the following error when the user accesses the attached virtual disk: The Recycle Bin is corrupted. Do you want to /// empty the Recycle Bin for this drive? /// /// public void Attach(ATTACH_VIRTUAL_DISK_FLAG flags, ATTACH_VIRTUAL_DISK_PARAMETERS? param = null, PSECURITY_DESCRIPTOR securityDescriptor = default) { if (!securityDescriptor.IsNull && !securityDescriptor.IsValidSecurityDescriptor()) { throw new ArgumentException("Invalid security descriptor."); } if (param.HasValue) AttachVirtualDisk(Handle, securityDescriptor, flags, 0, param.Value, IntPtr.Zero).ThrowIfFailed(); else AttachVirtualDisk(Handle, securityDescriptor, flags, 0, IntPtr.Zero, IntPtr.Zero).ThrowIfFailed(); } /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// Attach the virtual disk as read-only. /// /// If false, decouple the virtual disk lifetime from that of the VirtualDisk. The virtual disk will be attached until the Detach /// function is called, even if all open instances of the virtual disk are disposed. /// /// No drive letters are assigned to the disk's volumes. /// /// An optional pointer to a FileSecurity instance to apply to the attached virtual disk. If this parameter is , /// the security descriptor of the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to /// the attached virtual disk grants the write attributes permission for the user, or that the security descriptor of the virtual disk /// image file grants the write attributes permission for the user if you specify for this parameter. If the /// security descriptor does not grant write attributes permission for a user, Shell displays the following error when the user accesses /// the attached virtual disk: The Recycle Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// public void Attach(bool readOnly = false, bool autoDetach = true, bool noDriveLetter = false, FileSecurity? access = null) => Attach(noDriveLetter ? null : new[] { "*" }, readOnly, autoDetach, access); /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// /// The user-mode paths to be associated with the volume. These may be a drive letter (for example, "X:\") or a directory on another /// volume (for example, "Y:\MountX\"). The string must end with a trailing backslash ('\'). Use "*" as the first and only entry to have /// the system assign a drive letter or to leave the drive letter unassigned. /// /// Attach the virtual disk as read-only. /// /// If false, decouple the virtual disk lifetime from that of the VirtualDisk. The virtual disk will be attached until the Detach /// function is called, even if all open instances of the virtual disk are disposed. /// /// /// An optional pointer to a FileSecurity instance to apply to the attached virtual disk. If this parameter is , /// the security descriptor of the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to /// the attached virtual disk grants the write attributes permission for the user, or that the security descriptor of the virtual disk /// image file grants the write attributes permission for the user if you specify for this parameter. If the /// security descriptor does not grant write attributes permission for a user, Shell displays the following error when the user accesses /// the attached virtual disk: The Recycle Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// public void Attach(string[]? mountPoints, bool readOnly = false, bool autoDetach = true, FileSecurity? access = null) { if (mountPoints is not null && mountPoints.Length == 0) throw new ArgumentException("Mount points list cannot be empty.", nameof(mountPoints)); if (mountPoints is not null && mountPoints[0] != "*" && mountPoints.Any(p => p == "*")) throw new ArgumentException("Mount points may only contain the '*' value as the first and only value.", nameof(mountPoints)); ATTACH_VIRTUAL_DISK_FLAG flags = 0; if (mountPoints is null || (mountPoints.Length > 0 && mountPoints[0] != "*")) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER; if (readOnly) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY; if (!autoDetach) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME; if (access is null) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_NO_SECURITY_DESCRIPTOR; using SafePSECURITY_DESCRIPTOR sd = FileSecToSd(access); Attach(flags, ATTACH_VIRTUAL_DISK_PARAMETERS.Default, sd); if (mountPoints is null || mountPoints[0] is "*") return; var vgp = VolumeGuidPaths ?? new string[0]; if (mountPoints.Length > vgp.Length) { Detach(); throw new ArgumentException("The number of mount points cannot be larger than the number of associated volumes.", nameof(mountPoints)); } for (var i = 0; i < mountPoints.Length; i++) Win32Error.ThrowLastErrorIfFalse(SetVolumeMountPoint(mountPoints[i], vgp[i])); } /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// A valid combination of values of the ATTACH_VIRTUAL_DISK_FLAG enumeration. /// A reference to a valid ATTACH_VIRTUAL_DISK_PARAMETERS structure that contains attachment parameter data. /// /// An optional pointer to a SECURITY_DESCRIPTOR to apply to the attached virtual disk. If this parameter is NULL, the security /// descriptor of the virtual disk image file is used. /// /// Ensure that the security descriptor that AttachVirtualDisk applies to the attached virtual disk grants the write attributes /// permission for the user, or that the security descriptor of the virtual disk image file grants the write attributes permission for /// the user if you specify NULL for this parameter. If the security descriptor does not grant write attributes permission for a user, /// Shell displays the following error when the user accesses the attached virtual disk: The Recycle Bin is corrupted. Do you want to /// empty the Recycle Bin for this drive? /// /// /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// public async Task AttachAsync(ATTACH_VIRTUAL_DISK_FLAG flags, ATTACH_VIRTUAL_DISK_PARAMETERS? param = null, PSECURITY_DESCRIPTOR securityDescriptor = default, CancellationToken cancellationToken = default, IProgress? progress = default) { if (!securityDescriptor.IsNull && !securityDescriptor.IsValidSecurityDescriptor()) throw new ArgumentException("Invalid security descriptor."); await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => AttachVirtualDisk(Handle, securityDescriptor, flags, 0, param ?? ATTACH_VIRTUAL_DISK_PARAMETERS.Default, vhdOverlap), cancellationToken, progress); } /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// Attach the virtual disk as read-only. /// /// If false, decouple the virtual disk lifetime from that of the VirtualDisk. The virtual disk will be attached until the Detach /// function is called, even if all open instances of the virtual disk are disposed. /// /// No drive letters are assigned to the disk's volumes. /// /// An optional pointer to a FileSecurity instance to apply to the attached virtual disk. If this parameter is , /// the security descriptor of the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to /// the attached virtual disk grants the write attributes permission for the user, or that the security descriptor of the virtual disk /// image file grants the write attributes permission for the user if you specify for this parameter. If the /// security descriptor does not grant write attributes permission for a user, Shell displays the following error when the user accesses /// the attached virtual disk: The Recycle Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// public async Task AttachAsync(bool readOnly = false, bool autoDetach = true, bool noDriveLetter = false, FileSecurity? access = null, CancellationToken cancellationToken = default, IProgress? progress = default) => await AttachAsync(noDriveLetter ? null : new[] { "*" }, readOnly, autoDetach, access, cancellationToken, progress); /// /// Attaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate VHD provider to accomplish the attachment. /// /// /// The user-mode path to be associated with the volume. This may be a drive letter (for example, "X:\") or a directory on another volume /// (for example, "Y:\MountX\"). The string must end with a trailing backslash ('\'). Use "*" to have the system assign a drive letter or /// to leave the drive letter unassigned. /// /// Attach the virtual disk as read-only. /// /// If false, decouple the virtual disk lifetime from that of the VirtualDisk. The virtual disk will be attached until the Detach /// function is called, even if all open instances of the virtual disk are disposed. /// /// /// An optional pointer to a FileSecurity instance to apply to the attached virtual disk. If this parameter is , /// the security descriptor of the virtual disk image file is used. Ensure that the security descriptor that AttachVirtualDisk applies to /// the attached virtual disk grants the write attributes permission for the user, or that the security descriptor of the virtual disk /// image file grants the write attributes permission for the user if you specify for this parameter. If the /// security descriptor does not grant write attributes permission for a user, Shell displays the following error when the user accesses /// the attached virtual disk: The Recycle Bin is corrupted. Do you want to empty the Recycle Bin for this drive? /// /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// public async Task AttachAsync(string[]? mountPoints, bool readOnly = false, bool autoDetach = true, FileSecurity? access = null, CancellationToken cancellationToken = default, IProgress? progress = default) { if (mountPoints is not null && mountPoints.Length == 0) throw new ArgumentException("Mount points list cannot be empty.", nameof(mountPoints)); if (mountPoints is not null && mountPoints[0] != "*" && mountPoints.Any(p => p == "*")) throw new ArgumentException("Mount points may only contain the '*' value as the first and only value.", nameof(mountPoints)); ATTACH_VIRTUAL_DISK_FLAG flags = 0; if (mountPoints is null || (mountPoints.Length > 0 && mountPoints[0] != "*")) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER; if (readOnly) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY; if (!autoDetach) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME; if (access is null) flags |= ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_NO_SECURITY_DESCRIPTOR; using SafePSECURITY_DESCRIPTOR sd = FileSecToSd(access); await AttachAsync(flags, ATTACH_VIRTUAL_DISK_PARAMETERS.Default, sd, cancellationToken, progress); if (mountPoints is null || mountPoints[0] is "*") return; var vgp = VolumeGuidPaths ?? new string[0]; if (mountPoints.Length > vgp.Length) { Detach(); throw new ArgumentException("The number of mount points cannot be larger than the number of associated volumes.", nameof(mountPoints)); } for (var i = 0; i < mountPoints.Length; i++) Win32Error.ThrowLastErrorIfFalse(SetVolumeMountPoint(mountPoints[i], vgp[i])); } /// Breaks a previously initiated mirror operation and sets the mirror to be the active virtual disk. public void BreakMirror() => BreakMirrorVirtualDisk(Handle).ThrowIfFailed(); /// Closes the instance of the virtual disk. public void Close() => Dispose(); /// Reduces the size of a virtual hard disk (VHD) backing store file. public void Compact() => CompactVirtualDisk(Handle, COMPACT_VIRTUAL_DISK_FLAG.COMPACT_VIRTUAL_DISK_FLAG_NONE, COMPACT_VIRTUAL_DISK_PARAMETERS.Default, IntPtr.Zero).ThrowIfFailed(); /// Reduces the size of a virtual hard disk (VHD) backing store file. /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// true if operation completed without error or cancellation; false otherwise. public async Task CompactAsync(CancellationToken cancellationToken = default, IProgress? progress = default) => await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => CompactVirtualDisk(Handle, COMPACT_VIRTUAL_DISK_FLAG.COMPACT_VIRTUAL_DISK_FLAG_NONE, COMPACT_VIRTUAL_DISK_PARAMETERS.Default, vhdOverlap), cancellationToken, progress); /// Returns the value of the handle field. /// An IntPtr representing the value of the handle field. public IntPtr DangerousGetHandle() => ((IHandle)Handle).DangerousGetHandle(); /// Deletes a snapshot from a VHD Set file. /// The Snapshot Id in GUID format indicating which snapshot is to be deleted from the VHD Set. /// /// If set to , a reference point should be persisted in the VHD Set after the snapshot is deleted.. /// /// public void DeleteSnapshot(Guid id, bool persistReferencePoint = false) { DELETE_SNAPSHOT_VHDSET_PARAMETERS param = new() { Version = DELETE_SNAPSHOT_VHDSET_VERSION.DELETE_SNAPSHOT_VHDSET_VERSION_1, Version1 = new() { SnapshotId = id } }; DeleteSnapshotVhdSet(Handle, param, persistReferencePoint ? 0 : DELETE_SNAPSHOT_VHDSET_FLAG.DELETE_SNAPSHOT_VHDSET_FLAG_PERSIST_RCT).ThrowIfFailed(); } /// /// Detaches a virtual hard disk (VHD) or CD or DVD image file (ISO) by locating an appropriate virtual disk provider to accomplish the operation. /// public void Detach() { if (!Attached) { return; } DetachVirtualDisk(Handle, DETACH_VIRTUAL_DISK_FLAG.DETACH_VIRTUAL_DISK_FLAG_NONE, 0).ThrowIfFailed(); } /// public virtual void Dispose() => Handle.Dispose(); /// Increases the size of a fixed or dynamic virtual hard disk (VHD). /// New size, in bytes, for the expansion request. public void Expand(ulong newSize) { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) { throw new NotSupportedException(@"Expansion is only available to virtual disks opened under version 2 or higher."); } ExpandVirtualDisk(Handle, EXPAND_VIRTUAL_DISK_FLAG.EXPAND_VIRTUAL_DISK_FLAG_NONE, new EXPAND_VIRTUAL_DISK_PARAMETERS(newSize), IntPtr.Zero).ThrowIfFailed(); } /// Increases the size of a fixed or dynamic virtual hard disk (VHD). /// New size, in bytes, for the expansion request. /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// true if operation completed without error or cancellation; false otherwise. public async Task ExpandAsync(ulong newSize, CancellationToken cancellationToken = default, IProgress? progress = default) => await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) { throw new NotSupportedException(@"Expansion is only available to virtual disks opened under version 2 or higher."); } EXPAND_VIRTUAL_DISK_PARAMETERS param = new(newSize); return ExpandVirtualDisk(Handle, EXPAND_VIRTUAL_DISK_FLAG.EXPAND_VIRTUAL_DISK_FLAG_NONE, param, vhdOverlap); }, cancellationToken, progress); /// Issues an embedded SCSI request directly to a virtual hard disk. /// A valid RAW_SCSI_VIRTUAL_DISK_PARAMETERS structure that contains snapshot deletion data. /// A RAW_SCSI_VIRTUAL_DISK_RESPONSE structure that contains the results of processing the SCSI command. /// Only supported on Windows 10 and later. public RAW_SCSI_VIRTUAL_DISK_RESPONSE IssueSCSIRequest(in RAW_SCSI_VIRTUAL_DISK_PARAMETERS param) { if (!PInvokeClient.Windows10.IsPlatformSupported()) throw new PlatformNotSupportedException("Only supported on Windows 10 and later."); RawSCSIVirtualDisk(Handle, param, RAW_SCSI_VIRTUAL_DISK_FLAG.RAW_SCSI_VIRTUAL_DISK_FLAG_NONE, out RAW_SCSI_VIRTUAL_DISK_RESPONSE resp).ThrowIfFailed(); return resp; } /// Merges a child virtual hard disk (VHD) in a differencing chain with parent disks in the chain. /// Depth from the leaf from which to begin the merge. The leaf is at depth 1. /// Depth from the leaf to target the merge. The leaf is at depth 1. public void Merge(uint sourceDepth, uint targetDepth) { MERGE_VIRTUAL_DISK_PARAMETERS param = new(sourceDepth, targetDepth); MergeVirtualDisk(Handle, MERGE_VIRTUAL_DISK_FLAG.MERGE_VIRTUAL_DISK_FLAG_NONE, param, IntPtr.Zero).ThrowIfFailed(); } /// Asynchronously merges a child virtual hard disk (VHD) in a differencing chain with parent disks in the chain. /// Depth from the leaf from which to begin the merge. The leaf is at depth 1. /// Depth from the leaf to target the merge. The leaf is at depth 1. /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// public async Task MergeAsync(uint sourceDepth, uint targetDepth, CancellationToken cancellationToken = default, IProgress? progress = default) => await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => { MERGE_VIRTUAL_DISK_PARAMETERS param = new(sourceDepth, targetDepth); return MergeVirtualDisk(Handle, MERGE_VIRTUAL_DISK_FLAG.MERGE_VIRTUAL_DISK_FLAG_NONE, param, vhdOverlap); }, cancellationToken, progress); /// Merges a child virtual hard disk (VHD) in a differencing chain with its immediate parent disk in the chain. public void MergeWithParent() { MERGE_VIRTUAL_DISK_PARAMETERS param = new(1); MergeVirtualDisk(Handle, MERGE_VIRTUAL_DISK_FLAG.MERGE_VIRTUAL_DISK_FLAG_NONE, param, IntPtr.Zero).ThrowIfFailed(); } /// /// Initiates a mirror operation for a virtual disk. Once the mirroring operation is initiated it will not complete until either CancelIo /// or CancelIoEx is called to cancel all I/O on the VirtualDiskHandle, leaving the original file as the current or /// BreakMirrorVirtualDisk is called to stop using the original file and only use the mirror. /// /// Fully qualified path where the mirrored virtual disk will be or is located. /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// @"Mirroring is only available to virtual disks opened under version 2 or higher. public async Task MirrorAsync(string destPath, CancellationToken cancellationToken = default, IProgress? progress = default) { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) throw new NotSupportedException(@"Mirroring is only available to virtual disks opened under version 2 or higher."); await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => { MIRROR_VIRTUAL_DISK_FLAG flags = File.Exists(destPath) ? MIRROR_VIRTUAL_DISK_FLAG.MIRROR_VIRTUAL_DISK_FLAG_EXISTING_FILE : MIRROR_VIRTUAL_DISK_FLAG.MIRROR_VIRTUAL_DISK_FLAG_NONE; SafeCoTaskMemString pDest = new(destPath); MIRROR_VIRTUAL_DISK_PARAMETERS param = new() { Version = MIRROR_VIRTUAL_DISK_VERSION.MIRROR_VIRTUAL_DISK_VERSION_1, Version1 = new() { MirrorVirtualDiskPath = pDest } }; return MirrorVirtualDisk(Handle, flags, param, vhdOverlap); }, cancellationToken, progress); } /// Modifies the internal contents of a virtual disk file. Can be used to set the active leaf, or to fix up snapshot entries. /// The Snapshot Id in GUID format indicating which snapshot is to have its path altered in the VHD Set. /// The new file path for the Snapshot indicated by . /// to indicate that the snapshot should be created as a writable snapshot type. public void ModifySnapshotPath(Guid id, string newSnapshotFilePath, bool writeable = false) { using SafeCoTaskMemString pfp = new(newSnapshotFilePath); MODIFY_VHDSET_PARAMETERS param = new() { Version = MODIFY_VHDSET_VERSION.MODIFY_VHDSET_SNAPSHOT_PATH, Version1 = new() { SnapshotPath = new() { SnapshotId = id, SnapshotFilePath = (IntPtr)pfp } } }; ModifyVhdSet(Handle, param, writeable ? 0 : MODIFY_VHDSET_FLAG.MODIFY_VHDSET_FLAG_WRITEABLE_SNAPSHOT).ThrowIfFailed(); } /// /// Retrieves information about changes to the specified areas of a virtual hard disk (VHD) that are tracked by resilient change tracking (RCT). /// /// /// The change tracking identifier for the change that identifies the state of the virtual disk that you want to use as the basis of /// comparison to determine whether the specified area of the VHD has changed. /// /// /// The distance from the start of the VHD to the beginning of the area of the VHD that you want to check for changes, in bytes. /// /// The length of the area of the VHD that you want to check for changes, in bytes. /// /// An array of QUERY_CHANGES_VIRTUAL_DISK_RANGE structures that indicates the areas of the virtual disk within the area that the /// and parameters specify that have changed since the change tracking identifier /// that the parameter specifies was sealed. /// public QUERY_CHANGES_VIRTUAL_DISK_RANGE[] QueryChanges(string changeTrackingId, ulong offset = 0, ulong length = ulong.MaxValue) { if (length + offset > VirtualSize) length = VirtualSize - offset; var cRanges = 42u; ulong cProcessed; QUERY_CHANGES_VIRTUAL_DISK_RANGE[] ranges; do { cRanges *= 4; ranges = new QUERY_CHANGES_VIRTUAL_DISK_RANGE[cRanges]; QueryChangesVirtualDisk(Handle, changeTrackingId, offset, length, QUERY_CHANGES_VIRTUAL_DISK_FLAG.QUERY_CHANGES_VIRTUAL_DISK_FLAG_NONE, ranges, ref cRanges, out cProcessed).ThrowIfFailed(); } while (cProcessed < length); Array.Resize(ref ranges, (int)cRanges); return ranges; } /// Resizes a virtual disk. /// /// New size, in bytes, for the expansion request. Setting this value to '0' will shrink the disk to the smallest safe virtual size /// possible without truncating past any existing partitions. /// public void Resize(ulong newSize) { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) { throw new NotSupportedException(@"Expansion is only available to virtual disks opened under version 2 or higher."); } RESIZE_VIRTUAL_DISK_FLAG flags = newSize == 0 ? RESIZE_VIRTUAL_DISK_FLAG.RESIZE_VIRTUAL_DISK_FLAG_RESIZE_TO_SMALLEST_SAFE_VIRTUAL_SIZE : RESIZE_VIRTUAL_DISK_FLAG.RESIZE_VIRTUAL_DISK_FLAG_NONE; RESIZE_VIRTUAL_DISK_PARAMETERS param = new(newSize); ResizeVirtualDisk(Handle, flags, param, IntPtr.Zero).ThrowIfFailed(); } /// Resizes a virtual disk. /// New size, in bytes, for the expansion request. /// /// A cancellation token that can be used to cancel the operation. This value can be default to disable cancellation. /// /// /// A class that implements that can be used to report on progress. This value can be null to disable /// progress reporting. /// /// true if operation completed without error or cancellation; false otherwise. public async Task ResizeAsync(ulong newSize, CancellationToken cancellationToken = default, IProgress? progress = default) { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) { throw new NotSupportedException(@"Expansion is only available to virtual disks opened under version 2 or higher."); } return await RunAsync(Handle, (in NativeOverlapped vhdOverlap) => { RESIZE_VIRTUAL_DISK_PARAMETERS param = new(newSize); return ResizeVirtualDisk(Handle, RESIZE_VIRTUAL_DISK_FLAG.RESIZE_VIRTUAL_DISK_FLAG_NONE, param, vhdOverlap); }, cancellationToken, progress); } /// Sets the active leaf of a virtual disk file. /// The file path for the default Snapshot of the Vhd Set. /// to indicate that the snapshot should be created as a writable snapshot type. public void SetDefaultSnapshotPath(string defaultFilePath, bool writeable = false) { using SafeCoTaskMemString pfp = new(defaultFilePath); MODIFY_VHDSET_PARAMETERS param = new() { Version = MODIFY_VHDSET_VERSION.MODIFY_VHDSET_DEFAULT_SNAPSHOT_PATH, Version1 = new() { DefaultFilePath = (IntPtr)pfp } }; ModifyVhdSet(Handle, param, writeable ? 0 : MODIFY_VHDSET_FLAG.MODIFY_VHDSET_FLAG_WRITEABLE_SNAPSHOT).ThrowIfFailed(); } /// Sets the path to virtal hard disk's parent. /// The full path to the parent disk. public void SetParentPath(string path) { using SafeCoTaskMemString pStr = new(path); SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_PARENT_PATH) { ParentFilePath = (IntPtr)pStr }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } /// Sets the path to virtal hard disk's parent and the child depth. /// The full path to the parent disk. /// Specifies the depth to the child from the leaf. The leaf itself is at depth 1. public void SetParentPathAndDepth(string path, uint childDepth) { using SafeCoTaskMemString pStr = new(path); SET_VIRTUAL_DISK_INFO si = new(SET_VIRTUAL_DISK_INFO_VERSION.SET_VIRTUAL_DISK_INFO_PARENT_PATH_WITH_DEPTH) { ParentPathWithDepthInfo = new() { ParentFilePath = (IntPtr)pStr, ChildDepth = childDepth } }; SetVirtualDiskInformation(Handle, si).ThrowIfFailed(); } /// Creates a snapshot of the current virtual disk for VHD Set files. /// The Id of the new Snapshot to be added to the Vhd Set. /// to indicate that the snapshot should be created as a writable snapshot type. public void TakeSnapshot(Guid id, bool writeable = false) { TAKE_SNAPSHOT_VHDSET_PARAMETERS param = new() { Version = TAKE_SNAPSHOT_VHDSET_VERSION.TAKE_SNAPSHOT_VHDSET_VERSION_1, Version1 = new() { SnapshotId = id } }; TakeSnapshotVhdSet(Handle, param, writeable ? 0 : TAKE_SNAPSHOT_VHDSET_FLAG.TAKE_SNAPSHOT_VHDSET_FLAG_WRITEABLE).ThrowIfFailed(); } /// /// Resizes a virtual disk without checking the virtual disk's partition table to ensure that this truncation is safe. This method can cause unrecoverable data loss; use with care. /// /// New size, in bytes, for the expansion request. public void UnsafeResize(ulong newSize) { if (ver < OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_2) { throw new NotSupportedException(@"Expansion is only available to virtual disks opened under version 2 or higher."); } RESIZE_VIRTUAL_DISK_FLAG flags = RESIZE_VIRTUAL_DISK_FLAG.RESIZE_VIRTUAL_DISK_FLAG_ALLOW_UNSAFE_VIRTUAL_SIZE; RESIZE_VIRTUAL_DISK_PARAMETERS param = new(newSize); ResizeVirtualDisk(Handle, flags, param, IntPtr.Zero).ThrowIfFailed(); } private static SafePSECURITY_DESCRIPTOR FileSecToSd(FileSecurity? sec) => sec == null ? SafePSECURITY_DESCRIPTOR.Null : ConvertStringSecurityDescriptorToSecurityDescriptor(sec.GetSecurityDescriptorSddlForm(AccessControlSections.All)); private static uint? GetDiskNumberFromDevicePath(string devicePath) { using var hfile = OpenDrive(devicePath); return !hfile.IsInvalid && DeviceIoControl(hfile, IOControlCode.IOCTL_STORAGE_GET_DEVICE_NUMBER, out STORAGE_DEVICE_NUMBER output) ? output.DeviceNumber : null; } private static bool GetProgress(VIRTUAL_DISK_HANDLE hVhd, in NativeOverlapped vhdOverlap, CancellationToken cancellationToken, IProgress? progress) { progress?.Report(0); while (true) { if (cancellationToken.IsCancellationRequested) return false; GetVirtualDiskOperationProgress(hVhd, vhdOverlap, out VIRTUAL_DISK_PROGRESS prog).ThrowIfFailed(); switch (prog.OperationStatus) { case 0: progress?.Report(100); return true; case Win32Error.ERROR_IO_PENDING: progress?.Report(Math.Min(100, (int)(prog.CurrentValue * 100 / prog.CompletionValue))); break; default: throw new Win32Error(prog.OperationStatus).GetException() ?? new Exception(); } Task.Delay(100, cancellationToken); } } private static string? GetVolumeGuidFromDiskNumber(uint diskNo) => GetVolumeGuidsFromDiskNumber(diskNo).FirstOrDefault(); private static IEnumerable GetVolumeGuidsFromDiskNumber(uint diskNo) => EnumVolumes().Where(v => { using var hfile = OpenDrive(v.TrimEnd('\\')); return !hfile.IsInvalid && DeviceIoControl(hfile, IOControlCode.IOCTL_STORAGE_GET_DEVICE_NUMBER, out STORAGE_DEVICE_NUMBER output) && output.DeviceNumber == diskNo; }).ToArray(); /// Retrieves a list of drive letters and mounted folder paths for the specified volume. /// A volume GUID path for the volume. A volume GUID path is of the form "\\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\". /// The list of drive letters and mounted folder paths. private static string[] GetVolumeMountPoints(string volumeName) { bool ret = GetVolumePathNamesForVolumeName(volumeName, IntPtr.Zero, 0, out var sz); var err = Win32Error.GetLastError(); if (!ret && err != Win32Error.ERROR_MORE_DATA) throw err.GetException()!; if (ret) return new string[0]; using var mem = new SafeCoTaskMemHandle(sz * StringHelper.GetCharSize()); Win32Error.ThrowLastErrorIfFalse(GetVolumePathNamesForVolumeName(volumeName, mem, sz, out _)); return mem.ToStringEnum().ToArray(); } private static SafeHFILE OpenDrive(string fn) => CreateFile(fn, 0, FileShare.ReadWrite, default, FileMode.Open, 0); private static async Task RunAsync(VIRTUAL_DISK_HANDLE hVhd, RunAsyncMethod method, CancellationToken cancellationToken, IProgress? progress) => await Task.Run(() => { NativeOverlapped vhdOverlap = new(); method(in vhdOverlap).ThrowUnless(Win32Error.ERROR_IO_PENDING); return GetProgress(hVhd, vhdOverlap, cancellationToken, progress); }); private T GetInformation(GET_VIRTUAL_DISK_INFO_VERSION info, long offset = 0) { using SafeCoTaskMemStruct mem = GetInformation(info); offset += 8; // Add size of version if (typeof(T) == typeof(string[])) { if (mem.DangerousGetHandle().Offset(offset).ToStructure() == 0) { return (T)(object)new string[0]; } return (T)(object)mem.DangerousGetHandle().ToStringEnum(CharSet.Unicode, (int)offset).ToArray(); } return mem.DangerousGetHandle().Offset(offset).Convert((uint)(mem.Size - offset), CharSet.Unicode)!; } private SafeCoTaskMemStruct GetInformation(GET_VIRTUAL_DISK_INFO_VERSION info) { SafeCoTaskMemStruct mem = new(new GET_VIRTUAL_DISK_INFO { Version = info }); uint sz = mem.Size; Win32Error err = GetVirtualDiskInformation(Handle, ref sz, mem, out var req); if (err == Win32Error.ERROR_INSUFFICIENT_BUFFER) { mem.Size = sz; err = GetVirtualDiskInformation(Handle, ref sz, mem, out req); } Debug.WriteLineIf(err.Succeeded, $"GetVirtualDiskInformation: Id={info.ToString().Remove(0, 22)}; Unk={Marshal.ReadInt32(mem, 4)}; Sz={req}; Bytes={string.Join(" ", mem.DangerousGetHandle().ToIEnum((int)req / 4).Select(b => b.ToString("X8")).ToArray())}"); err.ThrowIfFailed(); return mem; } /// Supports getting and setting metadata on a virtual disk. /// public class VirtualDiskMetadata : Vanara.Collections.VirtualDictionary { private readonly VirtualDisk parent; private readonly bool supported; /// Initializes a new instance of the class. /// The VHD. internal VirtualDiskMetadata(VirtualDisk vhd) : base(false) { parent = vhd; supported = vhd.DiskType == DeviceType.Vhdx; } /// public override ICollection Keys { get { var ret = new Guid[0]; if (supported) { if (parent.Handle.IsClosed) { throw new InvalidOperationException("Virtual disk not valid."); } uint count = 0; Win32Error err = EnumerateVirtualDiskMetadata(parent.Handle, ref count, null); if (err != Win32Error.ERROR_MORE_DATA && err != Win32Error.ERROR_INSUFFICIENT_BUFFER) { err.ThrowIfFailed(); } if (count != 0) { ret = new Guid[count]; EnumerateVirtualDiskMetadata(parent.Handle, ref count, ret).ThrowIfFailed(); return ret; } } return ret; } } /// public override bool Remove(Guid key) { ThrowIfInvalid(); return DeleteVirtualDiskMetadata(parent.Handle, key).Succeeded; } /// public override bool TryGetValue(Guid key, out SafeCoTaskMemHandle value) { ThrowIfInvalid(); uint sz = 0; value = SafeCoTaskMemHandle.Null; Win32Error err = GetVirtualDiskMetadata(parent.Handle, key, ref sz, default); if (err != Win32Error.ERROR_MORE_DATA && err != Win32Error.ERROR_INSUFFICIENT_BUFFER) return false; SafeCoTaskMemHandle ret = new((int)sz); if (GetVirtualDiskMetadata(parent.Handle, key, ref sz, ret).Failed) return false; value = ret; return true; } /// protected override void SetValue(Guid key, SafeCoTaskMemHandle value) { ThrowIfInvalid(); SetVirtualDiskMetadata(parent.Handle, key, value.Size, value).ThrowIfFailed(); } private void ThrowIfInvalid() { if (!supported) throw new PlatformNotSupportedException(); if (parent.Handle.IsClosed) throw new InvalidOperationException("Virtual disk not valid."); } } }