CimDiskImage.psm1

#Requires -Version 5.1
#Requires -RunAsAdministrator

function Dismount-CimDiskImage {
    <#
        .SYNOPSIS
        Dismounts a cimfs disk image from your system.

        .DESCRIPTION
        When the volume DeviceId is supplied as a parameter it will remove the mount point if it exists and then dismount the cimfs disk image, will only work on cim files. It will also dismount cimfs images with no pount point.

        .PARAMETER DeviceId
        Specifies the device ID of the volume, an example of which is: \\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862d}\

        .INPUTS
        This function will take inputs via pipeline as string and by property name DeviceId

        .OUTPUTS
        None.

        .EXAMPLE
        PS> Dismount-CimDiskImage -DeviceId '\\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862d}\'
        Dismounts a volume by DeviceId
        .EXAMPLE
        PS> Dismount-CimDiskImage -DeviceId @('\\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862e}\', '\\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862d}\')
        Dismounts a list of multiple volumes by DeviceId
        .EXAMPLE
        PS> Get-CimDiskImage C:\MyMountPoint | Dismount-CimDiskImage
        Dismounts a volume by path
        .EXAMPLE
        PS> Get-CimDiskImage | Dismount-CimDiskImage
        Dismounts all Cimfs volumes
        .EXAMPLE
        PS> Get-CimInstance -ClassName win32_volume | Where-Object { $_.FileSystem -eq 'cimfs' } | Dismount-CimDiskImage
        Dismounts all Cimfs volumes

    #>

    [CmdletBinding()]

    Param (
        [Parameter(
            Position = 0,
            ValuefromPipelineByPropertyName = $true,
            ValuefromPipeline = $true,
            Mandatory = $true
        )]
        [System.String[]]$DeviceId
    )

    begin {
        Set-StrictMode -Version Latest
    } # begin
    process {
        #CimFS operations need Win32 API calls to make work, I can't find a lot of native powershell to do what we need.

        #loop through multiple DeviceIds
        foreach ($Id in $DeviceId) {
        
            #Grab details of the cimfs volume from the device ID
            $volume = Get-CimInstance -ClassName win32_volume | Where-Object { $_.DeviceID -eq $Id -and $_.FileSystem -eq 'cimfs' }
            if ($null -eq $volume) {
                Write-Error "Cound not find cimfs $Id on this computer"
                return
            }

            #Check if there is a mount point, if there is remove it. It's possible to have a volume attached without a mount point, but unlikely.
            if ($volume.DeviceID -ne $volume.Name) {
                #Get Delete mount point API call from kernel32.dll
                $removeMountPointSignature = @"
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool DeleteVolumeMountPoint(string mountPoint);
"@


                $mountPointRemove = Add-Type -MemberDefinition $removeMountPointSignature -Name "RemoveVolMntPnt" -Namespace Win32Functions -PassThru

                #Function only present for mocking reasons in Pester
                function mockremovemountpoint { $mountPointRemove::DeleteVolumeMountPoint($volume.Name) }
                $removeMountPointResult = mockremovemountpoint; $remMntPntErr = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
                #Should return True/False

                if (-not ($removeMountPointResult)) {
                    $remMntPntErrStr = "Could not remove mount point to {0} Error:'{1}' ErrorCode:{2}" -f $volume.Name, $remMntPntErr.Message, $remMntPntErr.NativeErrorCode
                    Write-Error $remMntPntErrStr
                    return
                }
            }

            #Use CIM (WMI) to dismount volume after the mount point is removed.
            #Function only present for mocking reasons in Pester
            function mockdismount { Invoke-CimMethod -InputObject $volume -MethodName DisMount -Arguments @{ Force = $true } }
            $disMountVolumeResult = mockdismount

            switch ($disMountVolumeResult.ReturnValue) {
                0 { break } #Success no action
                1 { Write-Error "Dismounting volume $($volume.DeviceId) failed with error 'Access Denied'"; break }
                2 { Write-Error "Dismounting volume $($volume.DeviceId) failed with error 'Volume Has Mount Points'"; break }
                3 { Write-Error "Dismounting volume $($volume.DeviceId) failed with error 'Volume Does Not Support The No-Autoremount State'"; break }
                4 { Write-Error "Dismounting volume $($volume.DeviceId) failed with error 'Force Option Required'"; break }
                Default { Write-Error "Dismounting volume $($volume.DeviceId) failed with unknown error. Consult https://docs.microsoft.com/previous-versions/windows/desktop/vdswmi/dismount-method-in-class-win32-volume for documentation" }
            }

            Write-Verbose "Volume $Id Removed"

        }

    } # process
    end {} # end
}  #function Dismount-CimDiskImage

function Get-CimDiskImage {
    <#
        .SYNOPSIS
        Gets information about mounted cimfs disk image(s) on your system.

        .DESCRIPTION
        When the volume DeviceId or Mount Point is supplied, information about that disk will be returned, if no parameters are supplied all cimfs disks will be returned.

        .PARAMETER DeviceId
        Specifies the device ID of the volume, an example of which is: \\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862d}\

        .PARAMETER Path
        Specifies the mount point of the volume, an example of which is: C:\MyMountPoint

        .INPUTS
        This function will take inputs via pipeline as string and by property name DeviceId

        .OUTPUTS
        Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_Volume

        .EXAMPLE
        PS> Get-CimDiskImage -DeviceId '\\?\Volume{d342880f-3a74-4a9a-be74-2c67e2b3862d}\'
        Returns the details for the cimfs volume with the spcified DeviceId
        .EXAMPLE
        PS> Get-CimDiskImage -Path C:\MyMountPoint
        Returns the details for the cimfs volume with the spcified Path
        .EXAMPLE
        PS> Get-CimDiskImage
        Returns details about all cimfs volumes currntly mounted.

    #>

    [CmdletBinding(DefaultParameterSetName = 'DeviceId')]

    Param (
        [Parameter(
            ParameterSetName = 'Path',
            Position = 0,
            ValuefromPipelineByPropertyName = $true,
            ValuefromPipeline = $true
        )]
        [Alias('Fullname', 'Name')]
        [System.String]$Path,

        [Parameter(
            ParameterSetName = 'DeviceId',
            ValuefromPipelineByPropertyName = $true
        )]
        [System.String]$DeviceId
    )

    begin {
        Set-StrictMode -Version Latest
    } # begin
    process {
        #Get All the cimfs volumes
        $volume = Get-CimInstance -ClassName win32_volume | Where-Object { $_.FileSystem -eq 'cimfs' }

        #Filter (or not) based on parameter, param sets used so you can't put both deviceID and path in as params
        switch ($false) {
            ( [String]::IsNullOrEmpty($Path) ) {
                $out = $volume | Where-Object { $_.Name.TrimEnd('\') -eq $Path.TrimEnd('\') }
                Write-Output $out
                break
            }
            ( [String]::IsNullOrEmpty($DeviceId) ) {
                $out = $volume | Where-Object { $_.DeviceId -eq $DeviceId }
                Write-Output $out
                break
            }
            Default {
                Write-Output $volume
            }
        }
    } # process
    end {} # end
}  #function Get-CimDiskImage

function Mount-CimDiskImage {
    <#
        .SYNOPSIS
        Mounts a cimfs disk image to your system.

        .DESCRIPTION
        This will mount a cim file to a drive letter or directory of your choosing, allowing you to browse the contents. Remember to use the -Passthru Parameter to get output

        .PARAMETER ImagePath
        Specifies the location of the cim file to be mounted.

        .PARAMETER DriveLetter
        Specifies the Drive letter which the cim file should be mounted to. It can be in the format 'X:' or 'X:\'

        .PARAMETER MountPath
        Specifies the local folder to which the cim file will be mounted. This folder needs to exist and be empty prior to attempting to mount a cim file to it.

        .PARAMETER PassThru
        Will output details of the mount operation to the pipeline. Otherwise there will be no output

        .INPUTS
        This function will take inputs via pipeline by type and property and by position.

        .OUTPUTS
        PSCustomObject containing 'DeviceId', 'FileSystem', 'Path' and 'Guid'

        .EXAMPLE
        PS> Mount-CimDiskImage -ImagePath C:\MyCimFile.cim -MountPath C:\MyMountPath -Passthru
        Mounts the Cim file to a local directory and sends the result to the pipeline
        .EXAMPLE
        PS> Mount-CimDiskImage C:\MyCimFile.cim C:\MyMountPath
        Mounts the Cim file to a local directory
        .EXAMPLE
        PS> Get-ChildItem C:\MyCimFile.cim | Mount-CimDiskImage -MountPath C:\MyMountPath -Passthru
        Mounts the Cim file to a local directory and sends the result to the pipeline
        .EXAMPLE
        PS> 'C:\MyCimFile.cim' | Mount-CimDiskImage -MountPath C:\MyMountPath

    #>

    [CmdletBinding()]

    Param (
        [Parameter(
            Position = 0,
            ValuefromPipelineByPropertyName = $true,
            ValuefromPipeline = $true,
            Mandatory = $true
        )]
        [Alias('FullName')]
        [System.String]$ImagePath,

        [Parameter(
            ParameterSetName = 'ByLetter',
            Position = 1,
            ValuefromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [System.String]$DriveLetter,

        [Parameter(
            ParameterSetName = 'ByPath',
            ValuefromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [System.String]$MountPath,

        [Parameter(
            ValuefromPipelineByPropertyName = $true
        )]
        [Switch]$PassThru
    )

    begin {
        Set-StrictMode -Version Latest
        #requires -RunAsAdministrator
    } # begin
    process {
        #CimFS operations need a lot of Win32 API calls to make work, I can't find a lot of native powershell to do what we need.

        #Is the file there
        If (-not (Test-Path $ImagePath)) {
            Write-Error "$ImagePath does not exist"
            return
        }

        switch ($PSCmdlet.ParameterSetName) {
            ByLetter {
                if ($DriveLetter -notmatch "^\w\:\\?$") {
                    Write-Error "$DriveLetter does not seem to be a drive letter. Example X: or X:\"
                    return
                }
                else {
                    $MountPath = $DriveLetter
                }
                break
            }
            ByPath {
                If (-not (Test-Path $MountPath)) {
                    Write-Error "$MountPath does not exist"
                    return
                }
                break
            }
            Default {}
        }

        #Let's get the full file information, we'll need it later
        $fileInfo = Get-ChildItem $ImagePath

        #Is it a Cim file?
        If ($fileInfo.Extension -ne '.cim') {
            Write-Error "$ImagePath is not a Cim file"
            return
        }

        #Grab some file information in named variables
        $fileName = $fileInfo.Name
        $folder = $fileInfo.Directory.FullName

        # Make sure the path ends with a single \ as the SetVolumeMountPoint api requires this
        $MountPath = $MountPath.TrimEnd('\') + '\'

        #We need to supply a random guid for the mount param (needs to be cast as a ref to interact with the API)
        $guid = (New-Guid).Guid
        [ref]$guidRef = $guid

        #Get the method from the Cimfs.dll (don't change formatting)
        $mountSignature = @"
[DllImport( "cimfs.dll", CharSet = CharSet.Unicode, SetLastError = true )] public static extern long CimMountImage(String imageContainingPath, String imageName, IntPtr mountImageFlags, ref Guid volumeId);
"@

        #Create object
        $CimFSMount = Add-Type -MemberDefinition $mountSignature -Name "CimFSMount" -Namespace Win32Functions -PassThru

        #This function is only here so I can mock it during pester testing.
        function mockmount {
            #Mount the volume image flag needs to be 0
            $CimFSMount::CimMountImage($folder, $fileName, 0, $guidRef)
        }
        $mountResult = mockmount; $mntErr = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
        If ($mountResult -ne 0) {
            $mntErrStr = "Mounting {0} to volume failed with Error:'{1} ErrorCode:{2}'" -f $ImagePath, $mntErr.Message , $mntErr.NativeErrorCode
            Write-Error $mntErrStr
            return
        }

        $volume = Get-CimInstance -ClassName win32_volume | Where-Object { $_.DeviceID -eq "\\?\Volume{$guid}\" }

        $mountPointSignature = @"
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool SetVolumeMountPoint(string lpszVolumeMountPoint, string lpszVolumeName);
"@


        $mountPoint = Add-Type -MemberDefinition $mountPointSignature -Name "CimMountPoint" -Namespace Win32Functions -PassThru

        #This function is only here so I can mock it during pester testing.
        function mockmountpoint { $mountPoint::SetVolumeMountPoint($MountPath, $volume.DeviceID) }
        $mpResult = mockmountpoint; $mpError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

        If (-not ($mpResult)) {
            $mpErrStr = "Mounting {0} to {1} failed with Error:'{2}' ErrorCode:{3}" -f $volume.DeviceId, $mountPath , $mpError.Message, $mpError.NativeErrorCode
            Write-Error $mpErrStr
            $volume.DeviceID | Dismount-CimDiskImage
            return
        }

        Write-Verbose "Mounted $ImagePath to $MountPath"

        #Dump out with no object if sucessful as per guidelines
        If (-not ($Passthru)) {
            return
        }

        #This should be all you need to find it again
        $out = [PSCustomObject]@{
            DeviceId   = $volume.DeviceID
            FileSystem = $volume.FileSystem
            Path       = $MountPath
            Guid       = $guid
        }

        Write-Output $out

    } # process
    end {} # end
}  #function Mount-CimDiskImage