added boolean conversion helper and replaced code in scripts to use it

This commit is contained in:
2026-10-01 11:16:23 -04:00
parent 2efb7116d8
commit a53dcfb6a9
5 changed files with 143 additions and 12 deletions
+4 -12
View File
@@ -21,18 +21,10 @@ If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) {
} }
# Normalize RMM text values without treating every nonempty string as true. # Normalize RMM text values without treating every nonempty string as true.
$DsaOptions = @{ Zip = $Zip; Upload = $Upload; DeleteAfterUpload = $DeleteAfterUpload } $DsaOptions = @{
ForEach ( $DsaOptionName in @('Zip', 'Upload', 'DeleteAfterUpload') ) { Zip = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip'
$DsaOptionValue = ([string]$DsaOptions[$DsaOptionName]).Trim().ToLowerInvariant() Upload = ConvertTo-RmmBoolean -Value $Upload -VariableName 'Upload'
If ( $DsaOptionValue -in @('true', '$true', 'yes', 'y', '1') ) { DeleteAfterUpload = ConvertTo-RmmBoolean -Value $DeleteAfterUpload -VariableName 'DeleteAfterUpload'
$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)
}
} }
$DsaZipEnabled = $DsaOptions.Zip $DsaZipEnabled = $DsaOptions.Zip
+20
View File
@@ -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. `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 ## Uploading files
After dot-sourcing `Tools.ps1`, scripts can upload a completed file to the Emberkom FileDrop: 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\Upload-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\Get-DSAManifest.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\Download-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\ConvertTo-RmmBoolean.Tests.ps1
``` ```
+39
View File
@@ -180,6 +180,45 @@ Function Get-Version {
Return [version]$Return.Trim('.') 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 # Determines whether a URL is valid
Function IsURLValid { Function IsURLValid {
param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL) param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL)
+69
View File
@@ -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."
+11
View File
@@ -8,6 +8,17 @@ $DsaErrors = $null
$null = [Management.Automation.Language.Parser]::ParseFile($DsaScriptUnderTest, [ref]$DsaTokens, [ref]$DsaErrors) $null = [Management.Automation.Language.Parser]::ParseFile($DsaScriptUnderTest, [ref]$DsaTokens, [ref]$DsaErrors)
If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) } 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 Assert-Dsa { param([bool]$Condition, [string]$Description) If (!$Condition) { Throw $Description } }
Function Download-File { Throw 'DSA must use Download-FileDirectly.' } Function Download-File { Throw 'DSA must use Download-FileDirectly.' }
Function Download-FileDirectly { Function Download-FileDirectly {