functions/_readExtensionLockFile.ps1

# <copyright file="_readExtensionLockFile.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

function _readExtensionLockFile {
    [CmdletBinding()]
    [OutputType([System.Collections.IDictionary])]
    param (
        [Parameter(Mandatory)]
        [string] $Path
    )

    if (!(Test-Path $Path)) {
        Write-Verbose "No extensions lock file found at '$Path'"
        return $null
    }

    try {
        $lock = Get-Content -Raw -Path $Path | ConvertFrom-Json -AsHashtable
    }
    catch {
        throw "The extensions lock file '$Path' is not valid JSON: $($_.Exception.Message)"
    }

    if ($lock -isnot [System.Collections.IDictionary] -or
        $lock['lockVersion'] -ne 1 -or
        ($null -ne $lock['extensions'] -and $lock['extensions'] -isnot [System.Collections.IDictionary])
    ) {
        throw "The extensions lock file '$Path' is not in a supported format (expected 'lockVersion' 1)"
    }

    # Return the locked extensions, keyed by name (which, as elsewhere, is not case-sensitive)
    $lockedExtensions = [ordered]@{}
    if ($null -ne $lock['extensions']) {
        foreach ($name in $lock['extensions'].Keys) {
            $lockedExtensions[$name] = $lock['extensions'][$name]
        }
    }

    # ZeroFailed 2.0.0 recorded the full path of a local extension. Make it relative to the lock file's folder, as it is
    # now recorded, so that a lock file written by that version still matches on the machine that wrote it.
    foreach ($entry in $lockedExtensions.Values) {
        if ($entry -is [System.Collections.IDictionary] -and
            $entry['source'] -is [System.Collections.IDictionary] -and
            $entry['source']['type'] -eq 'path' -and
            $entry['source']['path']
        ) {
            # A full path is a native one (e.g. 'C:\repo/module' or '/repo/module'), or one on a PowerShell drive
            # (e.g. 'Repos:/module')
            $lockedPath = $entry['source']['path']
            $psDriveInfo = $null
            if ([IO.Path]::IsPathRooted($lockedPath) -or $ExecutionContext.SessionState.Path.IsPSAbsolute($lockedPath, [ref]$psDriveInfo)) {
                try {
                    $entry['source']['path'] = _getExtensionLockPath -Path $lockedPath -BasePath (Split-Path -Parent $Path)
                }
                catch {
                    # A path on a drive that does not exist here (e.g. one written on another machine) cannot match, so it
                    # is left as it is, to be reported as a difference from the lock file
                    Write-Verbose "Not making the path of local extension '$lockedPath' relative: $($_.Exception.Message)"
                }
            }
        }
    }

    # The content of an installed extension is checked against its content hash, so an extension from a git or
    # PowerShell repository must have one. Without it, the extension's content would not be checked at all.
    $namesWithoutHash = @($lockedExtensions.Keys | Where-Object {
        $entry = $lockedExtensions[$_]
        $entry -is [System.Collections.IDictionary] -and
            $entry['source'] -is [System.Collections.IDictionary] -and
            $entry['source']['type'] -in 'git', 'psrepository' -and
            !$entry['contentHash']
    })
    if ($namesWithoutHash.Count -gt 0) {
        throw ("The extensions lock file '$Path' has no content hash for: $($namesWithoutHash -join ', '). " +
               "Run with the lock mode set to 'Write' to record it.")
    }

    return $lockedExtensions
}