lib/VersionParser.ps1

#############################################################################
# VersionParser.ps1 - Version Parsing Utilities
#############################################################################
# This module provides utilities for parsing and comparing version strings.
# Only 3-part semantic versions (major.minor.patch) are supported.
#############################################################################

function ConvertTo-Version
{
    <#
    .SYNOPSIS
    Converts a version string to a [Version] object with 3 parts (major.minor.patch).
    
    .DESCRIPTION
    Parses version strings and normalizes them to 3-part versions.
    Only supports versions with 0, 1, 2, or 3 numeric components.
    - "1" becomes 1.0.0
    - "1.2" becomes 1.2.0
    - "1.2.3" stays 1.2.3
    - Versions with 4+ components are truncated to first 3 parts
    
    .PARAMETER Value
    The version string to parse. Must contain only numeric components separated by dots.
    
    .OUTPUTS
    A [Version] object with major, minor, and build (patch) components.
    
    .EXAMPLE
    ConvertTo-Version "1.2.3" # Returns [Version]1.2.3
    ConvertTo-Version "2.0" # Returns [Version]2.0.0
    ConvertTo-Version "3" # Returns [Version]3.0.0
    #>

    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Value
    )

    # Handle invalid input
    if ([string]::IsNullOrWhiteSpace($Value)) {
        throw "Version value cannot be null or empty"
    }

    $dots = $Value.Split(".").Count - 1

    switch ($dots)
    {
        0
        {
            return [Version]"$Value.0.0"
        }
        1
        {
            return [Version]"$Value.0"
        }
        2
        {
            return [Version]$Value
        }
        default
        {
            # For versions with more than 2 dots, truncate to first 3 parts
            $parts = $Value.Split(".") | Select-Object -First 3
            return [Version]($parts -join ".")
        }
    }
}

function Test-ValidVersionPattern
{
    <#
    .SYNOPSIS
    Validates that a version pattern is safe and well-formed.
    
    .DESCRIPTION
    Checks version patterns for:
    - Valid format (vX, vX.Y, vX.Y.Z, or wildcard patterns like v1.*)
    - ReDoS safety (prevents malicious patterns that could cause regex catastrophic backtracking)
    
    .PARAMETER Pattern
    The version pattern to validate.
    
    .OUTPUTS
    Returns $true if the pattern is valid and safe, $false otherwise.
    
    .EXAMPLE
    Test-ValidVersionPattern "v1.0.0" # Returns $true
    Test-ValidVersionPattern "v1.*" # Returns $true
    Test-ValidVersionPattern "v1.*.0" # Returns $false (invalid)
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$Pattern
    )
    
    # Maximum length check to prevent DoS via extremely long patterns
    if ($Pattern.Length -gt 50) {
        return $false
    }
    
    # Only allow simple patterns: vX, vX.Y, vX.Y.Z, or wildcards like v1.*, v1.0.*
    # Uses possessive-style matching via atomic groups where possible
    if ($Pattern -match '^v\d{1,10}(\.\d{1,10}){0,2}(\.\*)?$' -or $Pattern -match '^v\d{1,10}\.\*$') {
        return $true
    }
    
    return $false
}

function Test-VersionIgnored {
    <#
    .SYNOPSIS
    Check if a version should be ignored based on the ignore-versions configuration.
    
    .PARAMETER Version
    The version string to check (e.g., "v1.0.0").
    
    .PARAMETER IgnoreVersions
    Array of version patterns to ignore.
    
    .OUTPUTS
    Returns $true if the version should be ignored, $false otherwise.
    #>

    param(
        [string]$Version,
        [string[]]$IgnoreVersions
    )
    
    if (-not $IgnoreVersions -or $IgnoreVersions.Count -eq 0) {
        return $false
    }
    
    foreach ($pattern in $IgnoreVersions) {
        # Exact match
        if ($Version -eq $pattern) {
            Write-Host "::debug::Ignoring version $Version (matches ignore pattern: $pattern)"
            return $true
        }
        
        # Support wildcard patterns (e.g., "v1.*" matches "v1.0.0", "v1.1.0", etc.)
        if ($pattern -match '\*') {
            $regexPattern = '^' + [regex]::Escape($pattern).Replace('\*', '.*') + '$'
            if ($Version -match $regexPattern) {
                Write-Host "::debug::Ignoring version $Version (matches wildcard pattern: $pattern)"
                return $true
            }
        }
    }
    
    return $false
}