private/core-license/Get-OSDeployLicenseFileRecord.ps1

function ConvertTo-OSDeployCanonicalLicenseValue {
    <#
    .SYNOPSIS
        Converts a license value to a deterministic comparison string
 
    .DESCRIPTION
        Recursively sorts object properties and array values so semantically identical license
        payloads can be compared independently of JSON formatting or property order.
 
    .PARAMETER InputObject
        Specifies the value to canonicalize.
 
    .PARAMETER ExcludeRootProperty
        Specifies top-level payload properties to exclude from the comparison.
 
    .PARAMETER IsRoot
        Indicates that InputObject is the root license payload.
 
    .EXAMPLE
        PS> ConvertTo-OSDeployCanonicalLicenseValue -InputObject $License
 
        Returns a deterministic string for the license payload.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.String. Returns the canonical comparison value.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-09-02
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [AllowNull()]
        [object]$InputObject,

        [string[]]$ExcludeRootProperty = @(),

        [switch]$IsRoot
    )

    if ($null -eq $InputObject) {
        return 'null'
    }

    if ($InputObject -is [System.Collections.IDictionary]) {
        $Properties = foreach ($Key in @($InputObject.Keys | Sort-Object)) {
            if ($IsRoot -and $Key -in $ExcludeRootProperty) {
                continue
            }
            $Name = ConvertTo-Json -InputObject ([string]$Key) -Compress
            $Value = ConvertTo-OSDeployCanonicalLicenseValue -InputObject $InputObject[$Key] -ExcludeRootProperty $ExcludeRootProperty
            '{0}:{1}' -f $Name, $Value
        }
        return '{' + ($Properties -join ',') + '}'
    }

    if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
        $Values = foreach ($Value in $InputObject) {
            ConvertTo-OSDeployCanonicalLicenseValue -InputObject $Value -ExcludeRootProperty $ExcludeRootProperty
        }
        return '[' + (@($Values | Sort-Object) -join ',') + ']'
    }

    $PropertyNames = @($InputObject.PSObject.Properties.Name)
    if ($PropertyNames.Count -gt 0 -and $InputObject -isnot [ValueType] -and $InputObject -isnot [string]) {
        $Properties = foreach ($NameValue in @($PropertyNames | Sort-Object)) {
            if ($IsRoot -and $NameValue -in $ExcludeRootProperty) {
                continue
            }
            $Name = ConvertTo-Json -InputObject ([string]$NameValue) -Compress
            $Value = ConvertTo-OSDeployCanonicalLicenseValue -InputObject $InputObject.$NameValue -ExcludeRootProperty $ExcludeRootProperty
            '{0}:{1}' -f $Name, $Value
        }
        return '{' + ($Properties -join ',') + '}'
    }

    ConvertTo-Json -InputObject $InputObject -Compress
}

function Get-OSDeployLicenseFileRecord {
    <#
    .SYNOPSIS
        Reads comparable records from a Recast Software license file
 
    .DESCRIPTION
        Parses a .license2 file and returns one comparison record per payload entry. Each record
        includes normalized GUID, signature hash, and expiration values plus exact and renewal
        fingerprints.
 
    .PARAMETER LiteralPath
        Specifies the literal path to the .license2 file.
 
    .EXAMPLE
        PS> Get-OSDeployLicenseFileRecord -LiteralPath 'C:\Licenses\license.license2'
 
        Returns records used to normalize and deduplicate the license directory.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. Returns one record per parseable payload
        entry that contains a valid Data.LicenseGuid value.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-09-02
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Mandatory)]
        [string]$LiteralPath
    )

    try {
        $File = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop
        $Payload = @(Get-Content -LiteralPath $File.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
    }
    catch {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Unable to parse $LiteralPath. $($_.Exception.Message)"
        return
    }

    foreach ($Entry in $Payload) {
        $LicenseGuid = [guid]::Empty
        if (-not $Entry.Data -or -not [guid]::TryParse([string]$Entry.Data.LicenseGuid, [ref]$LicenseGuid)) {
            continue
        }

        $SignatureHash = $null
        if (-not [string]::IsNullOrWhiteSpace([string]$Entry.Signature)) {
            $HashProvider = [System.Security.Cryptography.SHA256]::Create()
            try {
                $SignatureBytes = [System.Text.Encoding]::UTF8.GetBytes([string]$Entry.Signature)
                $SignatureHash = [System.BitConverter]::ToString($HashProvider.ComputeHash($SignatureBytes)).Replace('-', '').ToLowerInvariant()
            }
            finally {
                $HashProvider.Dispose()
            }
        }

        $Expiration = [datetime]::MinValue
        if (-not [string]::IsNullOrWhiteSpace([string]$Entry.Expiration)) {
            $ParsedExpiration = [datetime]::MinValue
            if ([datetime]::TryParse([string]$Entry.Expiration, [ref]$ParsedExpiration)) {
                $Expiration = $ParsedExpiration
            }
        }

        [pscustomobject]@{
            File               = $File
            EntryCount         = $Payload.Count
            LicenseGuid        = $LicenseGuid.ToString().ToLowerInvariant()
            SignatureHash      = $SignatureHash
            Expiration         = $Expiration
            ExactFingerprint   = ConvertTo-OSDeployCanonicalLicenseValue -InputObject $Entry -IsRoot
            RenewalFingerprint = ConvertTo-OSDeployCanonicalLicenseValue -InputObject $Entry -ExcludeRootProperty @('Expiration', 'Signature') -IsRoot
        }
    }
}