# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. Describe 'Argument transformation attribute on optional argument with explicit $null' -Tags "CI" { $tdefinition = @' using System; using System.Management.Automation; using System.Reflection; namespace MSFT_1407291 { [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class AddressTransformationAttribute : ArgumentTransformationAttribute { public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) { return (ulong) 42; } } [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesUInt64")] [OutputType(typeof(System.String))] public class Cmdlet1 : PSCmdlet { [Parameter(Mandatory = false)] [AddressTransformation] public ulong Address { get; set; } protected override void ProcessRecord() { WriteObject(Address); } } [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesObject")] [OutputType(typeof(System.String))] public class Cmdlet2 : PSCmdlet { [Parameter(Mandatory = false)] [AddressTransformation] public object Address { get; set; } protected override void ProcessRecord() { WriteObject(Address ?? "passed in null"); } } } '@ $mod = Add-Type -PassThru -TypeDefinition $tdefinition Import-Module $mod[0].Assembly -ErrorVariable ErrorImportingModule function Invoke-ScriptFunctionTakesObject { param([MSFT_1407291.AddressTransformation()] [Parameter(Mandatory = $false)] [object]$Address = "passed in null") return $Address } function Invoke-ScriptFunctionTakesUInt64 { param([MSFT_1407291.AddressTransformation()] [Parameter(Mandatory = $false)] [Uint64]$Address = 11) return $Address } It "There was no error importing the in-memory module" { $ErrorImportingModule | Should -BeNullOrEmpty } It "Script function takes object" { Invoke-ScriptFunctionTakesObject | Should -Be 42 } It "Script function takes uint64" { Invoke-ScriptFunctionTakesUInt64 | Should -Be 42 } It "csharp cmdlet takes object" { Invoke-CSharpCmdletTakesObject | Should -Be "passed in null" } It "csharp cmdlet takes uint64" { Invoke-CSharpCmdletTakesUInt64 | Should -Be 0 } It "script function takes object when parameter is null" { Invoke-ScriptFunctionTakesObject -Address $null | Should -Be 42 } It "script function takes unit64 when parameter is null" { Invoke-ScriptFunctionTakesUInt64 -Address $null | Should -Be 42 } It "script csharp cmdlet takes object when parameter is null" { Invoke-CSharpCmdletTakesObject -Address $null | Should -Be 42 } It "script csharp cmdlet takes uint64 when parameter is null" { Invoke-CSharpCmdletTakesUInt64 -Address $null | Should -Be 42 } } Describe "Custom type conversion in parameter binding" -Tags 'Feature' { BeforeAll { ## Prepare the script module $content = @' function Test-ScriptCmdlet { [CmdletBinding(DefaultParameterSetName = "File")] param( [Parameter(Mandatory, ParameterSetName = "File")] [System.IO.FileInfo] $File, [Parameter(Mandatory, ParameterSetName = "StartInfo")] [System.Diagnostics.ProcessStartInfo] $StartInfo ) if ($PSCmdlet.ParameterSetName -eq "File") { $File.Name } else { $StartInfo.FileName } } function Test-ScriptFunction { param( [System.IO.FileInfo] $File, [System.Diagnostics.ProcessStartInfo] $StartInfo ) if ($null -ne $File) { $File.Name } if ($null -ne $StartInfo) { $StartInfo.FileName } } '@ Set-Content -Path $TestDrive\module.psm1 -Value $content -Force ## Prepare the C# module $code = @' using System.IO; using System.Diagnostics; using System.Management.Automation; namespace Test { [Cmdlet("Test", "BinaryCmdlet", DefaultParameterSetName = "File")] public class TestCmdletCommand : PSCmdlet { [Parameter(Mandatory = true, ParameterSetName = "File")] public FileInfo File { get; set; } [Parameter(Mandatory = true, ParameterSetName = "StartInfo")] public ProcessStartInfo StartInfo { get; set; } protected override void ProcessRecord() { if (this.ParameterSetName == "File") { WriteObject(File.Name); } else { WriteObject(StartInfo.FileName); } } } } '@ if ($IsWindows) { $asmFile = [System.IO.Path]::GetTempFileName() + ".dll" } else { $asmFile = (Join-Path $env:HOME $([System.IO.Path]::GetRandomFileName() + ".dll")) } Add-Type -TypeDefinition $code -OutputAssembly $asmFile ## Helper function to execute script function Execute-Script { [CmdletBinding(DefaultParameterSetName = "Script")] param( [Parameter(Mandatory)] [powershell]$ps, [Parameter(Mandatory, ParameterSetName = "Script")] [string]$Script, [Parameter(Mandatory, ParameterSetName = "Command")] [string]$Command, [Parameter(Mandatory, ParameterSetName = "Command")] [string]$ParameterName, [Parameter(Mandatory, ParameterSetName = "Command")] [object]$Argument ) $ps.Commands.Clear() $ps.Streams.ClearStreams() if ($PSCmdlet.ParameterSetName -eq "Script") { $ps.AddScript($Script).Invoke() } else { $ps.AddCommand($Command).AddParameter($ParameterName, $Argument).Invoke() } } ## Helper command strings $changeToConstrainedLanguage = '$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"' $getLanguageMode = '$ExecutionContext.SessionState.LanguageMode' $importScriptModule = "Import-Module $TestDrive\module.psm1" $importCSharpModule = "Import-Module $asmFile" } AfterAll { ## Set the LanguageMode to force rebuilding the type conversion cache. ## This is needed because type conversions happen in the new powershell runspace with 'ConstrainedLanguage' mode ## will be put in the type conversion cache, and that may affect the default session. $ExecutionContext.SessionState.LanguageMode = "FullLanguage" } It "Custom type conversion in parameter binding is allowed in FullLanguage" { ## Create a powershell instance for the test $ps = [powershell]::Create() try { ## Import the modules in FullLanguage mode Execute-Script -ps $ps -Script $importScriptModule Execute-Script -ps $ps -Script $importCSharpModule $languageMode = Execute-Script -ps $ps -Script $getLanguageMode $languageMode | Should -Be 'FullLanguage' $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" $result1 | Should -Be "fileToUse" $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" $result2 | Should -Be "fileToUse" $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" $result3 | Should -Be "fileToUse" ## Conversion involves setting properties of an instance of the target type is allowed in FullLanguage mode $hashValue = @{ FileName = "filename"; Arguments = "args" } $psobjValue = [PSCustomObject] $hashValue ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties $result4 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue $result4 | Should -Be "filename" $result5 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue $result5 | Should -Be "filename" ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties $result6 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue $result6 | Should -Be "filename" $result7 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue $result7 | Should -Be "filename" ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties $result8 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue $result8 | Should -Be "filename" $result9 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue $result9 | Should -Be "filename" } finally { $ps.Dispose() } } It "Some custom type conversion in parameter binding is allowed for trusted cmdlets in ConstrainedLanguage" { ## Create a powershell instance for the test $ps = [powershell]::Create() try { ## Import the modules in FullLanguage mode Execute-Script -ps $ps -Script $importScriptModule Execute-Script -ps $ps -Script $importCSharpModule $languageMode = Execute-Script -ps $ps -Script $getLanguageMode $languageMode | Should -Be 'FullLanguage' ## Change to ConstrainedLanguage mode Execute-Script -ps $ps -Script $changeToConstrainedLanguage $languageMode = Execute-Script -ps $ps -Script $getLanguageMode $languageMode | Should -Be 'ConstrainedLanguage' $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" $result1 | Should -Be "fileToUse" $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" $result2 | Should -Be "fileToUse" $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" $result3 | Should -Be "fileToUse" ## If the conversion involves setting properties of an instance of the target type, ## then it's disallowed even for trusted cmdlets. $hashValue = @{ FileName = "filename"; Arguments = "args" } $psobjValue = [PSCustomObject] $hashValue ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script" } } finally { $ps.Dispose() } } It "Custom type conversion in parameter binding is NOT allowed for untrusted cmdlets in ConstrainedLanguage" { ## Create a powershell instance for the test $ps = [powershell]::Create() try { $languageMode = Execute-Script -ps $ps -Script $getLanguageMode $languageMode | Should -Be 'FullLanguage' ## Change to ConstrainedLanguage mode Execute-Script -ps $ps -Script $changeToConstrainedLanguage $languageMode = Execute-Script -ps $ps -Script $getLanguageMode $languageMode | Should -Be 'ConstrainedLanguage' ## Import the modules in ConstrainedLanguage mode Execute-Script -ps $ps -Script $importScriptModule Execute-Script -ps $ps -Script $importCSharpModule $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" $result1 | Should -Be $null $ps.Streams.Error.Count | Should -Be 1 $ps.Streams.Error[0].FullyQualifiedErrorId | Should -Be "ParameterArgumentTransformationError,Test-ScriptCmdlet" $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" $result2 | Should -Be $null $ps.Streams.Error.Count | Should -Be 1 $ps.Streams.Error[0].FullyQualifiedErrorId | Should -Be "ParameterArgumentTransformationError,Test-ScriptFunction" ## Binary cmdlets are always marked as trusted because only trusted assemblies can be loaded on DeviceGuard machine. $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" $result3 | Should -Be "fileToUse" ## Conversion that involves setting properties of an instance of the target type is disallowed. $hashValue = @{ FileName = "filename"; Arguments = "args" } $psobjValue = [PSCustomObject] $hashValue ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script" } ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties try { Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script" } try { Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue throw "Expected exception was not thrown!" } catch { $_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script" } } finally { $ps.Dispose() } } } Describe 'Roundtrippable Conversions for Bare-string Numeric Literals passed to [string] Parameters' -Tags CI { BeforeAll { $TestValues = @( @{ Argument = "34uy" } @{ Argument = "48y" } @{ Argument = "8s" } @{ Argument = "49us" } @{ Argument = "26" } @{ Argument = "28u" } @{ Argument = "24l" } @{ Argument = "32ul" } @{ Argument = "20d" } @{ Argument = "6n" } ) function Test-SimpleStringValue([string] $Value) { $Value } function Test-AdvancedStringValue { [CmdletBinding()] param( [string] $Value ) $Value } } It 'should correctly convert back to string in simple functions' -TestCases $TestValues { param($Argument) Invoke-Expression "Test-SimpleStringValue -Value $Argument" | Should -BeExactly $Argument } It 'should correctly convert back to string in advanced functions' -TestCases $TestValues { param($Argument) Invoke-Expression "Test-AdvancedStringValue -Value $Argument" | Should -BeExactly $Argument } }