Dictionaries/Dict.Windows/Includes/BCD.psm1

# TODO get or write a crescendo module for bcdedit.exe
# TODO use a cache to not call bcdedit each time

# trick seen @url https://github.com/PowerShell/Crescendo/issues/10#issuecomment-742784265
# and @url https://github.com/Devolutions/WaykAgent-ps/blob/fdca9595797b2318ff5904b9ec6f24f43d69174a/WaykNow/Private/BcdEdit.ps1
# to un-localize bcdedit
if ($IsWindows) {
    $TEMP = [io.path]::GetTempPath().TrimEnd('\')
    $system32Path = [System.Environment]::SystemDirectory
    $bcdEditLocation = "$system32Path/bcdedit.exe"
    Copy-Item "$bcdEditLocation" -Destination "$TEMP" -Force -ErrorAction SilentlyContinue
    if (Test-FileExist "$TEMP/bcdedit.exe") {
        $Script:bcdedit = "$TEMP/bcdedit.exe"
    } else {
        $bcdEditLocation = "$system32Path\\bcdedit.exe"
        Copy-Item "$bcdEditLocation" -Destination "$TEMP" -Force -ErrorAction Stop
        $Script:bcdedit = "$TEMP\\bcdedit.exe"
    }
}

function Parse-BCDeditEnum {
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)]$output
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        $title = $null
        $entries = @()
        $entry = @{}
        $prevline = $null
        $line = $null
        $lineno = 0
        ForEach ($line in $output) {
            Write-PwShFwOSDevel "line n°$($lineno++) = $line"
            # empty lines is the end of the object
            if ($line.Trim() -match "^$") {
                Write-PwShFwOSDevel "empty line"
                if (!([string]::IsNullOrEmpty($entry))) {
                    $entries += $entry
                }
                $title = $null
                $prevline = $null
                $entry = @()
                continue
            }
            # skip type underline
            if ($line -match "^---*") {
                $title = $prevline
                Write-PwShFwOSDevel "underline -> title = '$prevline'"
                continue
            }
            # type of the BCDEntry (text underlined)
            if ([string]::IsNullOrEmpty($title)) {
                Write-PwShFwOSDevel "type "
                $entry = @{}
                $entry.type = $title
            } else {
                Write-PwShFwOSDevel "data"
                # entry is of the form "key some value"
                if ($line -match "^\w") {
                    Write-PwShFwOSDevel "word"
                    $data = $line.split(' ',2).Trim()
                    $key = $data[0]
                    $value = $data[1]
                    $entry.Add($key,$value)
                } else {
                # entry is another value of the previous key
                # the form is " some other value"
                    Write-PwShFwOSDevel "value"
                    $value = $entry.$key
                    $entry.Remove($key)
                    $value += $line.Trim()
                    $entry.Add($key,$value)
                }
            }
            $prevline = $line
        }
        return $entries
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}

<#
.SYNOPSIS
List all BCD entries configured
 
.DESCRIPTION
List all BCD entries available
 
.EXAMPLE
An example
 
.NOTES
General notes
#>

function Get-BCDAllEntries {
    [CmdletBinding(DefaultParameterSetName = "STRING")]
    [OutputType([String])]
    Param (
        # Specify alternate BCD store
        [Parameter()][string]$BCDStoreFilename,
        # list entries with their GUID
        [Parameter(ParameterSetName = "GUID")][switch]$AsGUID,
        # list entries in their human-readable form if available
        [Parameter(ParameterSetName = "STRING")][switch]$AsString
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            'STRING' {
                $params = "/enum all"
            }
            'GUID' {
                $params = "/enum all /v"
            }
        }
        if ($BCDStoreFilename) {
            $params = "/store '$BCDStoreFilename' $params"
        }
        Write-PwShFwOSDevel "$Script:bcdedit $params"
        $ids = Invoke-Expression -Command "$Script:bcdedit $params" | select-string "^identifier" | ForEach-Object { ($_ -Split " +")[1] }
        return $ids
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}

function Get-BCDEnum {
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Specify alternate BCD store
        [Parameter()][string]$BCDStoreFilename,
        # list entries with their GUID
        [Parameter()][switch]$AsGUID,
        # type of enum to list
        [ValidateSet('active', 'firmware', 'bootapp', 'bootmgr', 'osloader', 'resume', 'inherit', 'all')]
        [Parameter()][string]$Type = "Active",
        # list all boot entries. synonym of -Type All
        [switch]$All
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        $params = ""
        if ($BCDStoreFilename) {
            $params += "/store '$BCDStoreFilename'"
        }
        if ($All) {
            $params += " /enum all"
        } else {
            $params += " /enum $Type"
        }
        if ($AsGuid) {
            $params += " /v"
        }

        $enum = @{}
        Write-PwShFwOSDevel "$Script:bcdedit $params"
        $ids = Invoke-Expression -Command "$Script:bcdedit $params" | select-string "^identifier" | ForEach-Object { ($_ -Split " +")[1] }
        foreach ($id in $ids) {
            $entry = Get-BCDEntryDetails -Id $id -AsGUID:$AsGUID -BCDStoreFilename:$BCDStoreFilename
            $enum.Add($id, $entry)
        }
        return $enum
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}


function Get-BCDEntryDetails {
    [CmdletBinding(DefaultParameterSetName = "STRING")]
    [OutputType([String])]
    Param (
        # Specify alternate BCD store
        [Parameter()][string]$BCDStoreFilename,
        # Specify identifier to get details
        [Parameter(Mandatory = $true)][string]$Id,
        # list entries with their GUID
        [Parameter(ParameterSetName = "GUID")][switch]$AsGUID,
        # list entries in their human-readable form if available
        [Parameter(ParameterSetName = "STRING")][switch]$AsString
    )
    Begin {
        Write-PwShFwOSEnterFunction
        $BootMgr = ((bcdedit /enum "{bootmgr}" /v | select-string "^id") -split " +")[1]
        $BootCurrent = ((bcdedit /enum "{current}" /v | select-string "^id") -split " +")[1]
    }

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            'STRING' {
                $params = "/enum '$Id'"
            }
            'GUID' {
                $params = "/enum '$Id' /v"
            }
        }
        if ($BCDStoreFilename) {
            $params = "/store '$BCDStoreFilename' $params"
        }
        # it seems that real keys/value pair all start with lowercase char
        # titles starts with uppercase
        $entry = [PSCustomObject]@{}
        # $entry.Add("id", $Id)
        # $entry | Add-Member -MemberType NoteProperty -Name "id" -value $Id
        Write-PwShFwOSDevel "$Script:bcdedit $params"
        Invoke-Expression -Command "$Script:bcdedit $params" | select-string "^[a-z ]" | ForEach-Object {
            [string]$key = ($_ -Split " +")[0]
            $value = ($_ -Split " +",2)[1]
            # Write-PwShFwOSDevel "key = '$key' / value = '$value'"
            # handle arrays
            if ([string]::IsNullOrEmpty($key)) {
                $key = $previousKey
                # [array]$aValue = @($entry.$key,$value)
                # Write-PwShFwOSDevel "key = '$key' / aValue = '$aValue'"
                # [array]$entry.$key += $aValue
                [array]$entry.$key += ,$value
            } else {
                # Write-PwShFwOSDevel "key = '$key' / value = '$value'"
                # $entry.Add($key,$value)
                $entry | Add-Member -MemberType NoteProperty -Name "$key" -value "$value"
            }
            $previousKey = $key
            # $previousValue = $value
        }
        # add Id honoring -AsGUID parameter
        switch ($PSCmdlet.ParameterSetName) {
            'STRING' {
                $entry | Add-Member -MemberType NoteProperty -Name "id" -value $Id
            }
            'GUID' {
                $entry | Add-Member -MemberType NoteProperty -Name "id" -value $entry.identifier
            }
        }
        # duplicate description key to label key to make it behave like Get-BCDEntry in linux dictionnary
        # if ($entry.ContainsKey("description")) {
        if ($entry.description) {
            # $entry.Add("label", $entry.description)
            $entry | Add-Member -MemberType NoteProperty -Name "label" -value $entry.description
        }
        # if ($entry.ContainsKey("path")) {
        if ($entry.path) {
            # $entry.Add("filename", $entry.path)
            $entry | Add-Member -MemberType NoteProperty -Name "filename" -value $entry.path
        }
        $entry | Add-Member -MemberType NoteProperty -Name "current" -value $false
        if ($entry.identifier -eq $BootMgr) { $entry.current = $true }
        if ($entry.identifier -eq $BootCurrent) { $entry.current = $true }
        return $entry
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}

<#
.SYNOPSIS
Get the BCD from system to an hastable
 
.DESCRIPTION
Read BCD configuration and convert it to a useable hashtable
 
.EXAMPLE
An example
 
.NOTES
General notes
#>

function Get-BCD {
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Specify alternate BCD store
        [Alias('File', 'Filename', 'BCD')]
        [Parameter()][string]$BCDStoreFilename
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        # $bcd = @{}
        # Foreach ($id in (Get-BCDAllEntries -BCDStoreFilename $BCDStoreFilename -AsGUID)) {
        # $bcd[$id] = Get-BCDEntryDetails -BCDStoreFilename $BCDStoreFilename -Id $id -AsGUID
        # }
        $bcd = @()
        Foreach ($id in (Get-BCDAllEntries -BCDStoreFilename $BCDStoreFilename -AsGUID)) {
            $bcd += Get-BCDEntryDetails -BCDStoreFilename $BCDStoreFilename -Id $id -AsGUID
        }
        return $bcd
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}

function Set-BCDBootSequence {
    [CmdletBinding(DefaultParameterSetName = "ADDFIRST")]
    [OutputType([String])]
    Param (
        # id of the BCD entry
        [Parameter(ParameterSetName = "ADDFIRST")]
        [Parameter(ParameterSetName = "ADDLAST")]
        [Parameter(ParameterSetName = "REMOVE")]
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$id,

        # Adds the specified entry identifier to the top of the boot sequence
        [Parameter(ParameterSetName = "ADDFIRST")][switch]$AddFirst,

        # Adds the specified entry identifier to the end of the boot sequence
        [Parameter(ParameterSetName = "ADDLAST")][switch]$AddLast,

        # Removes the specified entry identifier from the boot sequence
        [Parameter(ParameterSetName = "REMOVE")][switch]$Remove
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            'ADDFIRST' {
                $params = "/addfirst"
            }
            'ADDLAST' {
                $params = "/addlast"
            }
            'REMOVE' {
                $params = "/remove"
            }
        }
        # this command change boot order of the {current} namespace. That is the name space of the Windows menu. We want to change UEFI boot order
        # $rc = Execute-Command -exe $Script:bcdedit -args "/bootsequence $id $params"
        $rc = Execute-Command -exe $Script:bcdedit -args "--% /set {fwbootmgr} displayorder $id $params" -AsBool
        return $rc
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}

function Set-BCDDisplayOrder {
    [CmdletBinding(DefaultParameterSetName = "ADDFIRST")]
    [OutputType([String])]
    Param (
        # id of the BCD entry
        [Parameter(ParameterSetName = "ADDFIRST")]
        [Parameter(ParameterSetName = "ADDLAST")]
        [Parameter(ParameterSetName = "REMOVE")]
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$id,

        # Adds the specified entry identifier to the top of the boot sequence
        [Parameter(ParameterSetName = "ADDFIRST")][switch]$AddFirst,

        # Adds the specified entry identifier to the end of the boot sequence
        [Parameter(ParameterSetName = "ADDLAST")][switch]$AddLast,

        # Removes the specified entry identifier from the boot sequence
        [Parameter(ParameterSetName = "REMOVE")][switch]$Remove
    )
    Begin {
        Write-PwShFwOSEnterFunction
    }

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            'ADDFIRST' {
                $params = "/addfirst"
            }
            'ADDLAST' {
                $params = "/addlast"
            }
            'REMOVE' {
                $params = "/remove"
            }
        }
        # this command change boot order of the {current} namespace. That is the name space of the Windows menu. We want to change UEFI boot order
        # $rc = Execute-Command -exe $Script:bcdedit -args "/bootsequence $id $params"
        $rc = Execute-Command -exe $Script:bcdedit -args "--% /displayorder $id $params" -AsBool
        return $rc
    }

    End {
        Write-PwShFwOSLeaveFunction
    }
}