|
| 1 | +<# |
| 2 | +Verifies that Windows PowerShell native argument escaping preserves backslashes |
| 3 | +while protecting embedded quotation marks for execute-dynamic-code requests. |
| 4 | +#> |
| 5 | + |
| 6 | +Set-StrictMode -Version Latest |
| 7 | +$ErrorActionPreference = "Stop" |
| 8 | + |
| 9 | +[string[]]$ScriptPaths = @( |
| 10 | + "scripts/run-windows-e2e.ps1", |
| 11 | + "scripts/test-simulate-mouse-demo.ps1", |
| 12 | + "Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1" |
| 13 | +) |
| 14 | + |
| 15 | +function Get-FunctionDefinition { |
| 16 | + param( |
| 17 | + [string]$ScriptPath, |
| 18 | + [string]$FunctionName |
| 19 | + ) |
| 20 | + |
| 21 | + [string]$source = Get-Content -LiteralPath $ScriptPath -Raw -Encoding UTF8 |
| 22 | + [string]$marker = "function $FunctionName" |
| 23 | + [int]$start = $source.IndexOf($marker, [System.StringComparison]::Ordinal) |
| 24 | + if ($start -lt 0) { |
| 25 | + throw "${ScriptPath} does not define ${FunctionName}." |
| 26 | + } |
| 27 | + |
| 28 | + [int]$openBrace = $source.IndexOf("{", $start, [System.StringComparison]::Ordinal) |
| 29 | + [int]$depth = 0 |
| 30 | + for ([int]$index = $openBrace; $index -lt $source.Length; $index++) { |
| 31 | + if ($source[$index] -eq "{") { |
| 32 | + $depth++ |
| 33 | + } |
| 34 | + elseif ($source[$index] -eq "}") { |
| 35 | + $depth-- |
| 36 | + if ($depth -eq 0) { |
| 37 | + return $source.Substring($start, $index - $start + 1) |
| 38 | + } |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + throw "${ScriptPath} has an unclosed ${FunctionName} definition." |
| 43 | +} |
| 44 | + |
| 45 | +function Assert-Equal { |
| 46 | + param( |
| 47 | + [string]$Actual, |
| 48 | + [string]$Expected, |
| 49 | + [string]$Context |
| 50 | + ) |
| 51 | + |
| 52 | + if ($Actual -ne $Expected) { |
| 53 | + throw "${Context}: expected [$Expected], actual [$Actual]." |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +function Invoke-WindowsPowerShellNativeArgumentRoundTrip { |
| 58 | + param( |
| 59 | + [string]$Argument |
| 60 | + ) |
| 61 | + |
| 62 | + [string]$childScriptPath = Join-Path ([System.IO.Path]::GetTempPath()) ("uloop-native-argument-round-trip-" + [Guid]::NewGuid().ToString("N") + ".ps1") |
| 63 | + Set-Content -LiteralPath $childScriptPath -Value '[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($args[0])))' -Encoding UTF8 |
| 64 | + [string[]]$childArguments = @("-NoProfile", "-File", $childScriptPath, $Argument) |
| 65 | + [string[]]$escapedArguments = @($childArguments | ForEach-Object { |
| 66 | + ConvertTo-WindowsPowerShellNativeArgument -Argument $_ |
| 67 | + }) |
| 68 | + [string]$encodedOutput = & powershell.exe @escapedArguments |
| 69 | + if ($LASTEXITCODE -ne 0) { |
| 70 | + Remove-Item -LiteralPath $childScriptPath |
| 71 | + throw "Windows PowerShell native argument round trip failed." |
| 72 | + } |
| 73 | + |
| 74 | + Remove-Item -LiteralPath $childScriptPath |
| 75 | + return [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encodedOutput)) |
| 76 | +} |
| 77 | + |
| 78 | +[string[]]$definitions = @($ScriptPaths | ForEach-Object { |
| 79 | + Get-FunctionDefinition -ScriptPath $_ -FunctionName "ConvertTo-WindowsPowerShellNativeArgument" |
| 80 | +}) |
| 81 | + |
| 82 | +foreach ($definition in @($definitions | Select-Object -Skip 1)) { |
| 83 | + Assert-Equal -Actual $definition -Expected $definitions[0] -Context "All E2E scripts must use the same native argument escaping implementation" |
| 84 | +} |
| 85 | + |
| 86 | +[string]$temporaryFunctionPath = Join-Path ([System.IO.Path]::GetTempPath()) "uloop-native-argument-escaping.ps1" |
| 87 | +Set-Content -LiteralPath $temporaryFunctionPath -Value $definitions[0] -Encoding UTF8 |
| 88 | +. $temporaryFunctionPath |
| 89 | +Remove-Item -LiteralPath $temporaryFunctionPath |
| 90 | + |
| 91 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\"') -Expected '\\\"' -Context "Existing escaped quote" |
| 92 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example Name\') -Expected 'C:\Users\Example Name\\' -Context "Trailing backslash in quoted argument" |
| 93 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\\\"') -Expected '\\\\\\\"' -Context "Consecutive backslashes before quote" |
| 94 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example\Project') -Expected 'C:\Users\Example\Project' -Context "Ordinary Windows path" |
| 95 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '日本語\パス"値') -Expected '日本語\パス\"値' -Context "Japanese text" |
| 96 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example\') -Expected 'C:\Users\Example\' -Context "Unquoted path with trailing backslash" |
| 97 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\') -Expected '\' -Context "Unquoted standalone backslash" |
| 98 | +Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument "C:\Users\Example\`n") -Expected "C:\Users\Example\`n" -Context "Trailing backslash before line feed" |
| 99 | +if ($PSVersionTable.PSVersion.Major -lt 6) { |
| 100 | + Assert-Equal -Actual (Invoke-WindowsPowerShellNativeArgumentRoundTrip -Argument 'C:\Users\Example\') -Expected 'C:\Users\Example\' -Context "Native process receives unquoted path with trailing backslash" |
| 101 | + Assert-Equal -Actual (Invoke-WindowsPowerShellNativeArgumentRoundTrip -Argument 'C:\Users\Example Name\') -Expected 'C:\Users\Example Name\' -Context "Native process receives quoted path with trailing backslash" |
| 102 | +} |
| 103 | + |
| 104 | +Write-Host "PowerShell native argument escaping tests passed." |
0 commit comments