lib/InputValidation.ps1
|
############################################################################# # InputValidation.ps1 - Input parsing and validation functions ############################################################################# # This module contains functions for parsing and validating action inputs. # It handles normalization, type conversion, and validation of all inputs # passed to the action via the inputs environment variable. ############################################################################# <# .SYNOPSIS Normalizes check input values to standard format (error/warning/none). .DESCRIPTION Accepts boolean or string values and normalizes them to the standard check level format used throughout the action. .PARAMETER Value The input value to normalize (can be boolean string or level string). .PARAMETER Default The default value if the input is null or empty. .OUTPUTS String - One of: "error", "warning", "none" #> function ConvertTo-CheckLevel { param( [string]$Value, [string]$Default = "error" ) $normalized = ($Value ?? $Default).Trim().ToLower() # Map boolean values to error/none if ($normalized -eq "true") { return "error" } elseif ($normalized -eq "false") { return "none" } return $normalized } <# .SYNOPSIS Parses the ignore-versions input into an array of version patterns. .DESCRIPTION Supports multiple input formats: 1. Comma-separated: "v1.0.0, v2.0.0" 2. Line-separated (newlines): "v1.0.0\nv2.0.0" 3. JSON array: ["v1.0.0", "v2.0.0"] Each version pattern is validated using Test-ValidVersionPattern. .PARAMETER RawInput The raw input value from the action inputs. .OUTPUTS Array of validated version patterns. #> function ConvertTo-IgnoreVersionsList { param( $RawInput ) $ignoreVersions = @() if (-not $RawInput) { return $ignoreVersions } $rawVersions = @() # Check if it's a JSON array (either already parsed or as string) if ($RawInput -is [array]) { # Already parsed as array by ConvertFrom-Json $rawVersions = $RawInput } elseif ($RawInput -is [string]) { $trimmedInput = $RawInput.Trim() # Check if it looks like a JSON array if ($trimmedInput.StartsWith('[') -and $trimmedInput.EndsWith(']')) { try { $parsed = $trimmedInput | ConvertFrom-Json if ($parsed -is [array]) { $rawVersions = $parsed } } catch { Write-Host "::warning title=Invalid JSON in ignore-versions::Failed to parse JSON array. Treating as comma/newline-separated list." # Fall through to comma/newline parsing } } # If not parsed as JSON array, split by comma and newline if ($rawVersions.Count -eq 0 -and $trimmedInput) { $rawVersions = $trimmedInput -split '[,\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } } # Validate each version pattern using Test-ValidVersionPattern for ReDoS prevention foreach ($ver in $rawVersions) { $verTrimmed = "$ver".Trim() if (-not $verTrimmed) { continue } # Use safe validation function that prevents ReDoS attacks if (Test-ValidVersionPattern -Pattern $verTrimmed) { $ignoreVersions += $verTrimmed } else { Write-Host "::warning title=Invalid ignore-versions pattern::Pattern '$verTrimmed' does not match expected format (vX, vX.Y, vX.Y.Z, or wildcard like v1.*). Skipping." } } return $ignoreVersions } <# .SYNOPSIS Parses and validates all action inputs from JSON. .DESCRIPTION Reads the inputs JSON environment variable, parses all inputs with defaults, validates them, and returns a hashtable with all parsed values. .PARAMETER State The RepositoryState object to update with configuration values. .OUTPUTS Hashtable with all parsed and validated input values. .EXAMPLE $config = Read-ActionInput -State $script:State #> function Read-ActionInput { param( [Parameter(Mandatory)] [RepositoryState]$State ) # Read inputs from JSON environment variable if (-not $env:inputs) { Write-Host "::error::inputs environment variable is not set" return $null } try { $inputs = $env:inputs | ConvertFrom-Json } catch { Write-Host "::error::Failed to parse inputs JSON" return $null } # Parse token (treat empty/whitespace as not provided) $tokenInput = $inputs.token if ([string]::IsNullOrWhiteSpace($tokenInput)) { $token = $State.Token } else { $token = $tokenInput } # SECURITY: Mask the token if it was provided via input (may be different from env var) if (-not [string]::IsNullOrWhiteSpace($tokenInput) -and $tokenInput -ne $env:GITHUB_TOKEN) { Write-Host "::add-mask::$($tokenInput)" } # Parse check levels $checkMinorVersion = ConvertTo-CheckLevel -Value (($inputs.'check-minor-version' ?? "error") -as [string]) -Default "error" $checkReleases = ConvertTo-CheckLevel -Value (($inputs.'check-releases' ?? "error") -as [string]) -Default "error" $checkReleaseImmutability = ConvertTo-CheckLevel -Value (($inputs.'check-release-immutability' ?? "error") -as [string]) -Default "error" $checkMarketplace = ConvertTo-CheckLevel -Value (($inputs.'check-marketplace' ?? "error") -as [string]) -Default "error" # Parse boolean inputs $ignorePreviewReleases = (($inputs.'ignore-preview-releases' ?? "true") -as [string]).Trim() -eq "true" $autoFix = (($inputs.'auto-fix' ?? "false") -as [string]).Trim() -eq "true" # Parse string inputs $floatingVersionsUse = (($inputs.'floating-versions-use' ?? "tags") -as [string]).Trim().ToLower() # Parse ignore-versions list $ignoreVersions = ConvertTo-IgnoreVersionsList -RawInput $inputs.'ignore-versions' if ($ignoreVersions.Count -gt 0) { Write-Host "::debug::Ignoring versions: $($ignoreVersions -join ', ')" } # Return parsed config return @{ Token = $token CheckMinorVersion = $checkMinorVersion CheckReleases = $checkReleases CheckReleaseImmutability = $checkReleaseImmutability CheckMarketplace = $checkMarketplace IgnorePreviewReleases = $ignorePreviewReleases FloatingVersionsUse = $floatingVersionsUse AutoFix = $autoFix IgnoreVersions = $ignoreVersions } } <# .SYNOPSIS Validates parsed input values and returns errors if invalid. .DESCRIPTION Checks that all input values are within their allowed ranges/values. Returns an array of error messages, or empty array if all valid. .PARAMETER Config Hashtable of parsed input values from Read-ActionInput. .OUTPUTS Array of error message strings. Empty if all inputs are valid. #> function Test-ActionInput { param( [Parameter(Mandatory)] [hashtable]$Config ) $errors = @() if ($Config.CheckMinorVersion -notin @("error", "warning", "none")) { $errors += "::error title=Invalid configuration::check-minor-version must be 'error', 'warning', 'none', 'true', or 'false', got '$($Config.CheckMinorVersion)'" } if ($Config.CheckReleases -notin @("error", "warning", "none")) { $errors += "::error title=Invalid configuration::check-releases must be 'error', 'warning', 'none', 'true', or 'false', got '$($Config.CheckReleases)'" } if ($Config.CheckReleaseImmutability -notin @("error", "warning", "none")) { $errors += "::error title=Invalid configuration::check-release-immutability must be 'error', 'warning', 'none', 'true', or 'false', got '$($Config.CheckReleaseImmutability)'" } if ($Config.CheckMarketplace -notin @("error", "warning", "none")) { $errors += "::error title=Invalid configuration::check-marketplace must be 'error', 'warning', 'none', 'true', or 'false', got '$($Config.CheckMarketplace)'" } if ($Config.FloatingVersionsUse -notin @("tags", "branches")) { $errors += "::error title=Invalid configuration::floating-versions-use must be either 'tags' or 'branches', got '$($Config.FloatingVersionsUse)'" } return $errors } <# .SYNOPSIS Writes debug output showing all parsed input values. .PARAMETER Config Hashtable of parsed input values. #> function Write-InputDebugInfo { param( [Parameter(Mandatory)] [hashtable]$Config ) Write-Host "::debug::=== Parsed Input Values ===" Write-Host "::debug::auto-fix: $($Config.AutoFix)" Write-Host "::debug::check-minor-version: $($Config.CheckMinorVersion)" Write-Host "::debug::check-releases: $($Config.CheckReleases)" Write-Host "::debug::check-release-immutability: $($Config.CheckReleaseImmutability)" Write-Host "::debug::check-marketplace: $($Config.CheckMarketplace)" Write-Host "::debug::ignore-preview-releases: $($Config.IgnorePreviewReleases)" Write-Host "::debug::floating-versions-use: $($Config.FloatingVersionsUse)" Write-Host "::debug::ignore-versions: $($Config.IgnoreVersions -join ', ')" } <# .SYNOPSIS Writes debug output showing repository configuration. .PARAMETER State The RepositoryState object. .PARAMETER Config Hashtable of parsed input values. #> function Write-RepositoryDebugInfo { param( [Parameter(Mandatory)] [RepositoryState]$State, [Parameter(Mandatory)] [hashtable]$Config ) Write-Host "::debug::Repository: $($State.RepoOwner)/$($State.RepoName)" Write-Host "::debug::API URL: $($State.ApiUrl)" Write-Host "::debug::Server URL: $($State.ServerUrl)" Write-Host "::debug::Token available: $(if ($State.Token) { 'Yes' } else { 'No' })" Write-Host "::debug::Check releases: $($Config.CheckReleases)" Write-Host "::debug::Check release immutability: $($Config.CheckReleaseImmutability)" Write-Host "::debug::Floating versions use: $($Config.FloatingVersionsUse)" } <# .SYNOPSIS Validates that auto-fix mode has required token. .PARAMETER State The RepositoryState object. .PARAMETER AutoFix Whether auto-fix mode is enabled. .OUTPUTS Boolean - True if valid, False if auto-fix enabled without token. #> function Test-AutoFixRequirement { param( [Parameter(Mandatory)] [RepositoryState]$State, [Parameter(Mandatory)] [bool]$AutoFix ) if ($AutoFix) { if (-not $State.Token) { # Use Write-Host for GitHub Actions error annotation (visible in Actions UI) Write-Host "::error title=Auto-fix requires token::Auto-fix mode is enabled but no GitHub token is available. Please provide a token via the 'token' input or ensure GITHUB_TOKEN is available.%0A%0AExample:%0A - uses: jessehouwing/actions-semver-checker@v2%0A with:%0A auto-fix: true%0A token: `${{ secrets.GITHUB_TOKEN }}" return $false } Write-Host "::debug::Auto-fix mode enabled with token" } return $true } |