Public/Import-HDTRunState.ps1

function Import-HDTRunState {
    <#
        .SYNOPSIS
            Reads and validates the state document, for a resume.
 
        .DESCRIPTION
            Reads state.json through the injected IFileSystem, parses it, holds
            it to Assert-HDTRunStateDocument and rehydrates it into the same
            shape New-HDTRunState produces, so a resumed run and a fresh one are
            the same object to everything downstream.
 
            Rehydration matters in two places:
 
              variable becomes an ordered, CASE-INSENSITIVE dictionary, because
                        a rule or a step may spell HDTComputerName however it
                        likes and ConvertFrom-Json gives back a case-sensitive
                        object
              step becomes an array in index order, because a resume finds
                        the next step by index and must not depend on the order
                        the JSON happened to list them in
 
            A CORRUPT DOCUMENT IS A TERMINATING ERROR, NOT "no state". Treating
            an unreadable state.json as an absent one would restart the sequence
            from step 1 on a machine that is already half built - re-running a
            destructive step such as a partition wipe. So a truncated file, a
            missing runId or a schemaVersion from the future all throw
            HDTConfigurationError naming the file, and a missing file throws
            HDTStateNotFound, which is a different and recoverable answer.
 
            Parsing never uses ConvertFrom-Json -AsHashtable: it is PowerShell
            6+ only, and the engine runs under 5.1 in WinPE.
 
        .PARAMETER Path
            The state document to read.
 
        .PARAMETER FileSystem
            An IFileSystem - New-HDTFileSystem in production,
            New-HDTFakeFileSystem in a test.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
        .EXAMPLE
            $state = Import-HDTRunState -Path 'C:\HDT\state.json' -FileSystem (New-HDTFileSystem)
            $state.stepIndex
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $FileSystem
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $text = $null
    try {
        $text = $FileSystem.ReadAllText($Path)
    } catch {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path `
                    -Message 'the state document could not be read. A run with no state document starts from the beginning; one that cannot be read does not.' `
                    -ErrorId 'HDTStateNotFound' -Category ObjectNotFound))
    }

    $document = $null
    try {
        # Never -AsHashtable: it is PowerShell 6+ only and banned outright.
        $document = ConvertFrom-Json -InputObject $text
    } catch {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path `
                    -Message ("the state document is not valid JSON and cannot be trusted. A corrupt state document is not the same as no state document: resuming from the beginning would re-run steps that have already run. Underlying error: {0}" -f $_.Exception.Message)))
    }

    Assert-HDTRunStateDocument -Document $document -Path $Path

    $variable = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($property in @($document.variable.PSObject.Properties)) {
        $variable[$property.Name] = $property.Value
    }
    $document.variable = $variable

    $document.step = @(@($document.step) | Sort-Object -Property index)

    $document.stepIndex = [int] $document.stepIndex
    $document.leg = [int] $document.leg
    $document.seq = [long] $document.seq

    # ConvertFrom-Json under pwsh 7 rehydrates an ISO 8601 string into a
    # [datetime]; under Windows PowerShell 5.1 it leaves the string alone. The
    # engine writes state.json under 5.1 in WinPE and reads it under whichever
    # engine is running, so every timestamp is normalised back to the round-trip
    # string here - an imported document has one shape, not one per engine.
    $normalize = {
        param($Object, [string] $Name)

        if (@($Object.PSObject.Properties | ForEach-Object { $_.Name }) -notcontains $Name) {
            return
        }

        $value = $Object.$Name
        if ($value -is [datetime]) {
            $Object.$Name = $value.ToUniversalTime().ToString('o', [System.Globalization.CultureInfo]::InvariantCulture)
        }
    }

    & $normalize $document 'startedUtc'
    & $normalize $document 'updatedUtc'

    foreach ($step in @($document.step)) {
        & $normalize $step 'startedUtc'
        & $normalize $step 'endedUtc'
    }

    return $document
}