diff --git a/Get-DSAManifest.ps1 b/Get-DSAManifest.ps1 index f5d530e..132e8d9 100644 --- a/Get-DSAManifest.ps1 +++ b/Get-DSAManifest.ps1 @@ -21,18 +21,10 @@ If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) { } # Normalize RMM text values without treating every nonempty string as true. -$DsaOptions = @{ Zip = $Zip; Upload = $Upload; DeleteAfterUpload = $DeleteAfterUpload } -ForEach ( $DsaOptionName in @('Zip', 'Upload', 'DeleteAfterUpload') ) { - $DsaOptionValue = ([string]$DsaOptions[$DsaOptionName]).Trim().ToLowerInvariant() - If ( $DsaOptionValue -in @('true', '$true', 'yes', 'y', '1') ) { - $DsaOptions[$DsaOptionName] = $true - } - ElseIf ( $DsaOptionValue -in @('', 'false', '$false', 'no', 'n', '0') ) { - $DsaOptions[$DsaOptionName] = $false - } - Else { - Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $DsaOptionName) - } +$DsaOptions = @{ + Zip = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip' + Upload = ConvertTo-RmmBoolean -Value $Upload -VariableName 'Upload' + DeleteAfterUpload = ConvertTo-RmmBoolean -Value $DeleteAfterUpload -VariableName 'DeleteAfterUpload' } $DsaZipEnabled = $DsaOptions.Zip diff --git a/README.md b/README.md index aab82f5..e0ce4fa 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,25 @@ For download failures, use the standalone [Test-FileDownload.ps1](Test-FileDownl `Get-DSAManifest.ps1` uses `Download-FileDirectly`, which passes the original URL to BITS without preliminary `Get-AbsoluteURI` or `IsURLValid` requests. It accepts the same URL and optional `-File` arguments as `Download-File`, waits for completion, and throws on BITS errors. If `-File` is omitted, its temporary filename comes from the original URL; supply `-File` for extensionless URLs or URLs with query strings. Existing `Download-File` callers retain their previous behavior. Publish `Tools.ps1` together with `Get-DSAManifest.ps1` so the new helper is available. +## Converting RMM boolean variables + +After dot-sourcing `Tools.ps1`, use `ConvertTo-RmmBoolean` with any RMM variable: + +```powershell +$RestartEnabled = ConvertTo-RmmBoolean -Value $Restart -VariableName 'Restart' +If ( $RestartEnabled ) { + # Perform the requested restart. +} + +# The variable name is optional; positional and pipeline input also work. +$ZipEnabled = ConvertTo-RmmBoolean $Zip +$UploadEnabled = $Upload | ConvertTo-RmmBoolean +``` + +Each input returns a `System.Boolean`: `true`, `$true`, `yes`, `y`, and `1` become `$true`; `false`, `$false`, `no`, `n`, `0`, blank strings, and `$null` become `$false`. Case and surrounding whitespace are ignored, and native boolean values also work. Other values throw an error; `-VariableName` identifies the input in that error. The function returns the converted value without changing the original variable; assign the result wherever needed. + +`Get-DSAManifest.ps1` uses this helper for its three options. Publish it together with the updated `Tools.ps1` so the helper is available to the RMM caller. + ## Uploading files After dot-sourcing `Tools.ps1`, scripts can upload a completed file to the Emberkom FileDrop: @@ -44,4 +63,5 @@ Offline tests (no uploads or notifications): powershell.exe -NoProfile -File .\tests\Upload-File.Tests.ps1 powershell.exe -NoProfile -File .\tests\Get-DSAManifest.Tests.ps1 powershell.exe -NoProfile -File .\tests\Download-File.Tests.ps1 +powershell.exe -NoProfile -File .\tests\ConvertTo-RmmBoolean.Tests.ps1 ``` diff --git a/Tools.ps1 b/Tools.ps1 index 534650f..182481e 100644 --- a/Tools.ps1 +++ b/Tools.ps1 @@ -180,6 +180,45 @@ Function Get-Version { Return [version]$Return.Trim('.') } +# Convert an RMM text variable into an actual Boolean value. +Function ConvertTo-RmmBoolean { + <# + .SYNOPSIS + Converts an RMM text value to a System.Boolean. + .DESCRIPTION + Accepts true, $true, yes, y, or 1; false, $false, no, n, or 0. + Matching ignores case and surrounding whitespace. Null and empty values + return false. Unrecognized values throw instead of silently enabling an option. + .PARAMETER Value + The value to convert. Native Boolean values are also accepted. + .PARAMETER VariableName + Optional RMM variable name, without the leading $, for error messages. + .EXAMPLE + $ZipEnabled = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip' + .EXAMPLE + ConvertTo-RmmBoolean ' No ' + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] + [AllowNull()] + [AllowEmptyString()] + [string]$Value, + [string]$VariableName = 'Value' + ) + process { + $NormalizedValue = ([string]$Value).Trim().ToLowerInvariant() + If ( $NormalizedValue -in @('true', '$true', 'yes', 'y', '1') ) { + Return $true + } + If ( $NormalizedValue -in @('', 'false', '$false', 'no', 'n', '0') ) { + Return $false + } + Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $VariableName) + } +} + # Determines whether a URL is valid Function IsURLValid { param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL) diff --git a/tests/ConvertTo-RmmBoolean.Tests.ps1 b/tests/ConvertTo-RmmBoolean.Tests.ps1 new file mode 100644 index 0000000..3fabb59 --- /dev/null +++ b/tests/ConvertTo-RmmBoolean.Tests.ps1 @@ -0,0 +1,69 @@ +#Requires -Version 5.0 +# Offline tests: load only the helper to avoid Tools.ps1 setup and logging. +$ErrorActionPreference = 'Stop' +$BooleanToolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Tools.ps1' +$BooleanTokens = $null +$BooleanParseErrors = $null +$BooleanAst = [Management.Automation.Language.Parser]::ParseFile($BooleanToolsPath, [ref]$BooleanTokens, [ref]$BooleanParseErrors) +If ($BooleanParseErrors.Count) { Throw ($BooleanParseErrors | Out-String) } +$BooleanDefinition = $BooleanAst.Find({ + param($Node) + $Node -is [Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq 'ConvertTo-RmmBoolean' +}, $true) +If (!$BooleanDefinition) { Throw 'Missing helper: ConvertTo-RmmBoolean' } +. ([scriptblock]::Create($BooleanDefinition.Extent.Text)) + +# Tools.ps1 overrides Write-Output with a string parameter. Boolean results must +# bypass that wrapper so $false remains a Boolean, without log text or side effects. +Function Write-Output { Throw 'Conversion must not use the string-based logging wrapper.' } + +$BooleanCases = @( + @{Values=@('true', '$true', 'yes', 'y', '1', $true, 1); Expected=$true} + @{Values=@('false', '$false', 'no', 'n', '0', '', $null, $false, 0); Expected=$false} +) +Foreach ($Case in $BooleanCases) { + Foreach ($Value in $Case.Values) { + $Variants = @($Value) + If ($Value -is [string]) { $Variants += " `t$($Value.ToUpperInvariant())`r`n " } + Foreach ($Variant in $Variants) { + $Result = @(ConvertTo-RmmBoolean -Value $Variant) + If ($Result.Count -ne 1 -or $Result[0] -isnot [bool] -or $Result[0] -ne $Case.Expected) { + Throw "Input '$Variant' must return exactly one Boolean with value $($Case.Expected)." + } + } + } +} +Write-Host 'PASS: accepted text, case, whitespace, null, native booleans, and numeric 0/1 return one Boolean.' + +$RmmInput = ' no ' +$Result = ConvertTo-RmmBoolean $RmmInput +If ($Result -isnot [bool] -or $Result -ne $false -or $RmmInput -cne ' no ') { + Throw 'Positional conversion must return false without changing the original input.' +} +$Result = @(' yes ', 'false', '', $null, '$TRUE', '0' | ConvertTo-RmmBoolean) +$Expected = @($true, $false, $false, $false, $true, $false) +If ($Result.Count -ne $Expected.Count) { Throw 'Pipeline conversion must return one value for every input.' } +For ($Index = 0; $Index -lt $Expected.Count; $Index++) { + If ($Result[$Index] -isnot [bool] -or $Result[$Index] -ne $Expected[$Index]) { + Throw "Pipeline conversion returned the wrong type or value at index $Index." + } +} +Write-Host 'PASS: positional input, preserved caller value, and multiple pipeline inputs.' + +# RMM scripts may use Continue and omit -ErrorAction. Invalid input must still stop. +$ErrorActionPreference = 'Continue' +Foreach ($Invalid in @('maybe', 'on', 'off', '2', 'true false', '$null')) { + Foreach ($Parameters in @(@{}, @{VariableName='Restart'})) { + $Caught = $null + $Returned = @() + Try { ConvertTo-RmmBoolean -Value $Invalid @Parameters | ForEach-Object { $Returned += $_ } } + Catch { $Caught = $_ } + $Name = If ($Parameters.ContainsKey('VariableName')) { 'Restart' } Else { 'Value' } + If ($null -eq $Caught -or !$Caught.Exception.Message.StartsWith(('Invalid ${0} value.' -f $Name))) { + Throw "Invalid input '$Invalid' must throw an error identifying $Name." + } + If ($Returned.Count -ne 0) { Throw 'Invalid input must not return a value.' } + } +} +Write-Host 'PASS: invalid input throws with default or caller-supplied variable name.' +Write-Host "All ConvertTo-RmmBoolean tests passed on PowerShell $($PSVersionTable.PSVersion), $([IntPtr]::Size * 8)-bit." diff --git a/tests/Get-DSAManifest.Tests.ps1 b/tests/Get-DSAManifest.Tests.ps1 index b379580..dbd2562 100644 --- a/tests/Get-DSAManifest.Tests.ps1 +++ b/tests/Get-DSAManifest.Tests.ps1 @@ -8,6 +8,17 @@ $DsaErrors = $null $null = [Management.Automation.Language.Parser]::ParseFile($DsaScriptUnderTest, [ref]$DsaTokens, [ref]$DsaErrors) If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) } +# Load only the real conversion helper, without running Tools.ps1 setup. +$DsaToolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Tools.ps1' +$DsaToolsAst = [Management.Automation.Language.Parser]::ParseFile($DsaToolsPath, [ref]$DsaTokens, [ref]$DsaErrors) +If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) } +$DsaBooleanDefinition = $DsaToolsAst.Find({ + param($Node) + $Node -is [Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq 'ConvertTo-RmmBoolean' +}, $true) +If (!$DsaBooleanDefinition) { Throw 'Missing helper: ConvertTo-RmmBoolean' } +. ([scriptblock]::Create($DsaBooleanDefinition.Extent.Text)) + Function Assert-Dsa { param([bool]$Condition, [string]$Description) If (!$Condition) { Throw $Description } } Function Download-File { Throw 'DSA must use Download-FileDirectly.' } Function Download-FileDirectly {