Private/VersionComparison.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    Version comparison functions for DriverManagement module
#>


function Compare-DriverVersion {
    <#
    .SYNOPSIS
        Compares two driver version strings
    .DESCRIPTION
        Normalizes and compares version strings, handling various formats
    .PARAMETER InstalledVersion
        Currently installed version
    .PARAMETER CatalogVersion
        Available version from catalog
    .RETURNS
        -1 if installed is older, 0 if same, 1 if installed is newer
    .EXAMPLE
        Compare-DriverVersion -InstalledVersion "1.0.0.0" -CatalogVersion "1.0.1.0"
        # Returns -1 (installed is older)
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$InstalledVersion,
        
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$CatalogVersion
    )
    
    # Handle empty versions
    if ([string]::IsNullOrWhiteSpace($InstalledVersion)) {
        return -1  # Treat missing as older
    }
    if ([string]::IsNullOrWhiteSpace($CatalogVersion)) {
        return 1   # Treat missing catalog as installed being newer
    }
    
    # Normalize versions - strip non-numeric/non-dot characters
    $v1Clean = $InstalledVersion -replace '[^0-9.]', ''
    $v2Clean = $CatalogVersion -replace '[^0-9.]', ''
    
    # Handle more than 4 parts (take first 4 for [Version] compatibility)
    $v1Parts = ($v1Clean.Split('.') | Select-Object -First 4) -join '.'
    $v2Parts = ($v2Clean.Split('.') | Select-Object -First 4) -join '.'
    
    # Ensure minimum Major.Minor format
    if ($v1Parts -notmatch '\.') { $v1Parts = "$v1Parts.0" }
    if ($v2Parts -notmatch '\.') { $v2Parts = "$v2Parts.0" }
    
    # Pad to ensure both have same number of parts
    $v1Segments = $v1Parts.Split('.')
    $v2Segments = $v2Parts.Split('.')
    $maxSegments = [Math]::Max($v1Segments.Count, $v2Segments.Count)
    
    while ($v1Segments.Count -lt $maxSegments) { $v1Segments += '0' }
    while ($v2Segments.Count -lt $maxSegments) { $v2Segments += '0' }
    
    $v1Parts = $v1Segments -join '.'
    $v2Parts = $v2Segments -join '.'
    
    try {
        $ver1 = [version]$v1Parts
        $ver2 = [version]$v2Parts
        
        if ($ver1 -lt $ver2) { return -1 }      # Installed is older
        elseif ($ver1 -gt $ver2) { return 1 }   # Installed is newer
        else { return 0 }                        # Same version
    }
    catch {
        # Fallback to string comparison
        return [string]::Compare($v1Parts, $v2Parts)
    }
}

function Compare-DriverDate {
    <#
    .SYNOPSIS
        Compares driver dates
    .DESCRIPTION
        For drivers that use date-based versioning
    .PARAMETER InstalledDate
        Date of installed driver
    .PARAMETER CatalogDate
        Date of catalog driver
    .RETURNS
        -1 if installed is older, 0 if same, 1 if installed is newer
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        $InstalledDate,
        
        [Parameter(Mandatory)]
        [AllowNull()]
        $CatalogDate
    )
    
    # Convert to DateTime if needed
    try {
        $date1 = if ($InstalledDate -is [datetime]) { $InstalledDate } 
                 elseif ($InstalledDate) { [datetime]$InstalledDate }
                 else { [datetime]::MinValue }
        
        $date2 = if ($CatalogDate -is [datetime]) { $CatalogDate }
                 elseif ($CatalogDate) { [datetime]$CatalogDate }
                 else { [datetime]::MaxValue }
        
        if ($date1 -lt $date2) { return -1 }
        elseif ($date1 -gt $date2) { return 1 }
        else { return 0 }
    }
    catch {
        return 0  # Treat as equal if comparison fails
    }
}

function Test-DriverNeedsUpdate {
    <#
    .SYNOPSIS
        Determines if a driver needs updating
    .DESCRIPTION
        Compares installed driver against catalog entry
    .PARAMETER InstalledDriver
        Object with DriverVersion and optionally DriverDate
    .PARAMETER CatalogDriver
        Object with Version and optionally ReleaseDate
    .RETURNS
        Hashtable with NeedsUpdate boolean and reason
    .EXAMPLE
        $result = Test-DriverNeedsUpdate -InstalledDriver $installed -CatalogDriver $catalog
        if ($result.NeedsUpdate) { Write-Host "Update available: $($result.Reason)" }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$InstalledDriver,
        
        [Parameter(Mandatory)]
        [object]$CatalogDriver
    )
    
    # Get version strings
    $installedVersion = if ($InstalledDriver.DriverVersion) { $InstalledDriver.DriverVersion }
                        elseif ($InstalledDriver.Version) { $InstalledDriver.Version }
                        else { "" }
    
    $catalogVersion = if ($CatalogDriver.Version) { $CatalogDriver.Version }
                      elseif ($CatalogDriver.DriverVersion) { $CatalogDriver.DriverVersion }
                      else { "" }
    
    # Compare versions
    $versionComparison = Compare-DriverVersion -InstalledVersion $installedVersion -CatalogVersion $catalogVersion
    
    if ($versionComparison -lt 0) {
        return @{
            NeedsUpdate = $true
            Reason = "Version outdated"
            InstalledVersion = $installedVersion
            CatalogVersion = $catalogVersion
            Comparison = "Installed ($installedVersion) < Catalog ($catalogVersion)"
        }
    }
    elseif ($versionComparison -gt 0) {
        return @{
            NeedsUpdate = $false
            Reason = "Installed is newer than catalog"
            InstalledVersion = $installedVersion
            CatalogVersion = $catalogVersion
            Comparison = "Installed ($installedVersion) > Catalog ($catalogVersion)"
        }
    }
    else {
        # Versions equal - check dates as tiebreaker
        $installedDate = $InstalledDriver.DriverDate
        $catalogDate = $CatalogDriver.ReleaseDate
        
        if ($installedDate -and $catalogDate) {
            $dateComparison = Compare-DriverDate -InstalledDate $installedDate -CatalogDate $catalogDate
            
            if ($dateComparison -lt 0) {
                return @{
                    NeedsUpdate = $true
                    Reason = "Same version but catalog has newer build date"
                    InstalledVersion = $installedVersion
                    CatalogVersion = $catalogVersion
                }
            }
        }
        
        return @{
            NeedsUpdate = $false
            Reason = "Up to date"
            InstalledVersion = $installedVersion
            CatalogVersion = $catalogVersion
        }
    }
}

function Get-VersionFromInf {
    <#
    .SYNOPSIS
        Extracts version information from INF file
    .PARAMETER InfPath
        Path to the INF file
    .RETURNS
        Hashtable with Version and Date
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$InfPath
    )
    
    if (-not (Test-Path $InfPath)) {
        return @{ Version = $null; Date = $null }
    }
    
    $content = Get-Content $InfPath -Raw
    
    # Look for DriverVer directive
    # Format: DriverVer = mm/dd/yyyy,x.x.x.x
    $driverVerMatch = [regex]::Match($content, 'DriverVer\s*=\s*(\d{1,2}/\d{1,2}/\d{4})\s*,\s*([\d.]+)')
    
    if ($driverVerMatch.Success) {
        $dateStr = $driverVerMatch.Groups[1].Value
        $version = $driverVerMatch.Groups[2].Value
        
        try {
            $date = [datetime]::ParseExact($dateStr, 'M/d/yyyy', $null)
        }
        catch {
            $date = $null
        }
        
        return @{
            Version = $version
            Date = $date
            Raw = $driverVerMatch.Value
        }
    }
    
    return @{ Version = $null; Date = $null }
}