src/Private/Device/RenderKit.DeviceService.ps1

function Get-RenderKitDevicesPath {
    return Join-Path (Get-RenderKitRoot) "Devices.json"
}

function New-RenderKitDeviceRegistry {
    return [PSCustomObject]@{
        Version       = "1.0"
        VolumeNames   = @()
        SerialNumbers = @()
    }
}

function Read-RenderKitDeviceRegistry {
    $path = Get-RenderKitDevicesPath

    if (!(Test-Path $path)) {
        $emptyRegistry = New-RenderKitDeviceRegistry
        Write-RenderKitDeviceRegistry -Registry $emptyRegistry | Out-Null
        Write-RenderKitLog -Level Info -Message "Created empty device whitelist at '$path'."
        return $emptyRegistry
    }

    try {
        $registry = Get-Content -Path $path -Raw | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        Write-RenderKitLog -Level Error -Message "Invalid JSON in device whitelist '$path'."
        throw "Invalid JSON in device whitelist '$path'."
    }

    $version = "1.0"
    if (
        $registry.PSObject.Properties.Name -contains "Version" -and
        -not [string]::IsNullOrWhiteSpace($registry.Version)
    ) {
        $version = [string]$registry.Version
    }

    $volumeNames = @()
    if ($registry.PSObject.Properties.Name -contains "VolumeNames") {
        $volumeNames = @(
            $registry.VolumeNames |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                ForEach-Object { ([string]$_).Trim() } |
                Sort-Object -Unique
        )
    }

    $serialNumbers = @()
    if ($registry.PSObject.Properties.Name -contains "SerialNumbers") {
        $serialNumbers = @(
            $registry.SerialNumbers |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                ForEach-Object { ([string]$_).Trim() } |
                Sort-Object -Unique
        )
    }

    return [PSCustomObject]@{
        Version       = $version
        VolumeNames   = $volumeNames
        SerialNumbers = $serialNumbers
    }
}

function Write-RenderKitDeviceRegistry {
    param(
        [Parameter(Mandatory)]
        [object]$Registry
    )

    $path = Get-RenderKitDevicesPath

    $version = "1.0"
    if (
        $Registry.PSObject.Properties.Name -contains "Version" -and
        -not [string]::IsNullOrWhiteSpace($Registry.Version)
    ) {
        $version = [string]$Registry.Version
    }

    $volumeNames = @()
    if ($Registry.PSObject.Properties.Name -contains "VolumeNames") {
        $volumeNames = @(
            $Registry.VolumeNames |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                ForEach-Object { ([string]$_).Trim() } |
                Sort-Object -Unique
        )
    }

    $serialNumbers = @()
    if ($Registry.PSObject.Properties.Name -contains "SerialNumbers") {
        $serialNumbers = @(
            $Registry.SerialNumbers |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                ForEach-Object { ([string]$_).Trim() } |
                Sort-Object -Unique
        )
    }

    $normalizedRegistry = [PSCustomObject]@{
        Version       = $version
        VolumeNames   = $volumeNames
        SerialNumbers = $serialNumbers
    }

    $normalizedRegistry |
        ConvertTo-Json -Depth 10 |
        Set-Content -Path $path -Encoding UTF8

    return $normalizedRegistry
}

function Resolve-RenderKitDriveLetter {
    param(
        [Parameter(Mandatory)]
        [string]$DriveLetter
    )

    $value = $DriveLetter.Trim()
    if ([string]::IsNullOrWhiteSpace($value)) {
        return $null
    }

    $value = $value.TrimEnd('\')
    if ($value.Length -eq 1) {
        $value = "${value}:"
    }
    elseif ($value.Length -gt 2 -and $value[1] -eq ':') {
        $value = $value.Substring(0, 2)
    }

    return $value.ToUpperInvariant()
}

function Get-RenderKitFileSystemPriority {
    param(
        [string]$FileSystem
    )

    if ([string]::IsNullOrWhiteSpace($FileSystem)) {
        return 0
    }

    switch ($FileSystem.ToUpperInvariant()) {
        "EXFAT" { return 2 }
        "FAT32" { return 1 }
        default { return 0 }
    }
}

function Get-RenderKitMountedDrive {
    [CmdletBinding()]
    param(
        [switch]$IncludeFixed,
        [switch]$IncludeUnsupportedFileSystem
    )

    $allowedDriveTypes = @(2) # Removable
    if ($IncludeFixed) {
        $allowedDriveTypes += 3 # Fixed
    }

    try {
        $logicalDisks = Get-CimInstance -ClassName Win32_LogicalDisk -ErrorAction Stop
    }
    catch {
        Write-RenderKitLog -Level Error -Message "Unable to query mounted drives. $($_.Exception.Message)"
        throw "Unable to query mounted drives. $_"
    }

    $result = @()
    foreach ($disk in $logicalDisks) {
        $driveType = [int]$disk.DriveType
        if ($allowedDriveTypes -notcontains $driveType) {
            continue
        }

        $fileSystem = [string]$disk.FileSystem
        $fileSystemPriority = Get-RenderKitFileSystemPriority -FileSystem $fileSystem

        if (-not $IncludeUnsupportedFileSystem -and $fileSystemPriority -eq 0) {
            continue
        }

        $sizeGB = $null
        if ($disk.Size) {
            $sizeGB = [Math]::Round(([double]$disk.Size / 1GB), 2)
        }

        $freeGB = $null
        if ($disk.FreeSpace) {
            $freeGB = [Math]::Round(([double]$disk.FreeSpace / 1GB), 2)
        }

        $result += [PSCustomObject]@{
            DriveLetter        = [string]$disk.DeviceID
            VolumeName         = [string]$disk.VolumeName
            FileSystem         = $fileSystem
            FileSystemPriority = $fileSystemPriority
            VolumeSerialNumber = [string]$disk.VolumeSerialNumber
            DriveType          = $driveType
            IsRemovable        = $driveType -eq 2
            SizeGB             = $sizeGB
            FreeGB             = $freeGB
        }
    }

    return $result
}

function Get-RenderKitDriveCandidatesInternal {
    [CmdletBinding()]
    param(
        [switch]$IncludeFixed,
        [switch]$IncludeUnsupportedFileSystem
    )

    $registry = Read-RenderKitDeviceRegistry
    $drives = Get-RenderKitMountedDrive `
        -IncludeFixed:$IncludeFixed `
        -IncludeUnsupportedFileSystem:$IncludeUnsupportedFileSystem

    $candidates = @()
    foreach ($drive in $drives) {
        $isWhitelistedVolumeName = $false
        if (-not [string]::IsNullOrWhiteSpace($drive.VolumeName)) {
            $isWhitelistedVolumeName = $registry.VolumeNames -contains $drive.VolumeName
        }

        $isWhitelistedSerialNumber = $false
        if (-not [string]::IsNullOrWhiteSpace($drive.VolumeSerialNumber)) {
            $isWhitelistedSerialNumber = $registry.SerialNumbers -contains $drive.VolumeSerialNumber
        }

        $score = 0
        if ($drive.FileSystemPriority -eq 2) {
            $score += 20
        }
        elseif ($drive.FileSystemPriority -eq 1) {
            $score += 10
        }

        if ($drive.IsRemovable) {
            $score += 15
        }

        if ($isWhitelistedVolumeName) {
            $score += 40
        }

        if ($isWhitelistedSerialNumber) {
            $score += 80
        }

        $candidates += [PSCustomObject]@{
            DriveLetter               = $drive.DriveLetter
            VolumeName                = $drive.VolumeName
            FileSystem                = $drive.FileSystem
            FileSystemPriority        = $drive.FileSystemPriority
            IsSupportedFileSystem     = $drive.FileSystemPriority -gt 0
            VolumeSerialNumber        = $drive.VolumeSerialNumber
            DriveType                 = $drive.DriveType
            IsRemovable               = $drive.IsRemovable
            SizeGB                    = $drive.SizeGB
            FreeGB                    = $drive.FreeGB
            IsWhitelistedVolumeName   = $isWhitelistedVolumeName
            IsWhitelistedSerialNumber = $isWhitelistedSerialNumber
            Score                     = $score
        }
    }

    return $candidates | Sort-Object `
        @{ Expression = "Score"; Descending = $true }, `
        @{ Expression = "FileSystemPriority"; Descending = $true }, `
        @{ Expression = "DriveLetter"; Descending = $false }
}