Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,19 @@ jobs:
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/install.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/check-release-installer.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-check-release-installer.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/run-windows-e2e.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-simulate-mouse-demo.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1))
$null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-powershell51-native-argument-escaping.ps1))

- name: Test release installer smoke helper
shell: powershell
run: .\scripts\test-check-release-installer.ps1

- name: Test Windows PowerShell native argument escaping
shell: powershell
run: .\scripts\test-powershell51-native-argument-escaping.ps1

test-unity-package:
runs-on: ubuntu-latest
needs: build-cli
Expand Down
29 changes: 27 additions & 2 deletions Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,40 @@ function Get-UloopArguments {
return @($CommandArguments + @("--project-path", $ResolvedProjectPath))
}

function ConvertTo-WindowsPowerShellNativeArgument {
param(
[string]$Argument
)

# Windows argv rules treat backslashes specially only before a quote or the closing quote.
# Do not double every backslash: ordinary paths such as C:\Users must stay unchanged.
[string]$escapedArgument = [regex]::Replace($Argument, '(\\*)"', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes + '\"'
})

if ($Argument -notmatch '\s') {
return $escapedArgument
}

return [regex]::Replace($escapedArgument, '(\\*)\z', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes
})
}

function Invoke-UloopCapture {
param(
[string[]]$CommandArguments
)

[string[]]$arguments = Get-UloopArguments -CommandArguments $CommandArguments
if ($PSVersionTable.PSVersion.Major -lt 6) {
# Windows PowerShell strips embedded quote characters from native arguments unless they are escaped.
$arguments = @($arguments | ForEach-Object { $_.Replace('"', '\"') })
$arguments = @($arguments | ForEach-Object { ConvertTo-WindowsPowerShellNativeArgument -Argument $_ })
}

[string]$previousErrorActionPreference = $ErrorActionPreference
Expand Down
29 changes: 27 additions & 2 deletions scripts/run-windows-e2e.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,40 @@ function Get-UloopArguments {
return $arguments
}

function ConvertTo-WindowsPowerShellNativeArgument {
param(
[string]$Argument
)

# Windows argv rules treat backslashes specially only before a quote or the closing quote.
# Do not double every backslash: ordinary paths such as C:\Users must stay unchanged.
[string]$escapedArgument = [regex]::Replace($Argument, '(\\*)"', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes + '\"'
})

if ($Argument -notmatch '\s') {
return $escapedArgument
}

return [regex]::Replace($escapedArgument, '(\\*)\z', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes
})
}

function Invoke-UloopCapture {
param(
[string[]]$CommandArguments
)

[string[]]$arguments = Get-UloopArguments -CommandArguments $CommandArguments
if ($PSVersionTable.PSVersion.Major -lt 6) {
# Windows PowerShell strips embedded quotes before native processes see them.
$arguments = @($arguments | ForEach-Object { $_.Replace('"', '\"') })
$arguments = @($arguments | ForEach-Object { ConvertTo-WindowsPowerShellNativeArgument -Argument $_ })
}

[string]$previousErrorActionPreference = $ErrorActionPreference
Expand Down
104 changes: 104 additions & 0 deletions scripts/test-powershell51-native-argument-escaping.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<#
Verifies that Windows PowerShell native argument escaping preserves backslashes
while protecting embedded quotation marks for execute-dynamic-code requests.
#>

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

[string[]]$ScriptPaths = @(
"scripts/run-windows-e2e.ps1",
"scripts/test-simulate-mouse-demo.ps1",
"Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1"
)

function Get-FunctionDefinition {
param(
[string]$ScriptPath,
[string]$FunctionName
)

[string]$source = Get-Content -LiteralPath $ScriptPath -Raw -Encoding UTF8
[string]$marker = "function $FunctionName"
[int]$start = $source.IndexOf($marker, [System.StringComparison]::Ordinal)
if ($start -lt 0) {
throw "${ScriptPath} does not define ${FunctionName}."
}

[int]$openBrace = $source.IndexOf("{", $start, [System.StringComparison]::Ordinal)
[int]$depth = 0
for ([int]$index = $openBrace; $index -lt $source.Length; $index++) {
if ($source[$index] -eq "{") {
$depth++
}
elseif ($source[$index] -eq "}") {
$depth--
if ($depth -eq 0) {
return $source.Substring($start, $index - $start + 1)
}
}
}

throw "${ScriptPath} has an unclosed ${FunctionName} definition."
}

function Assert-Equal {
param(
[string]$Actual,
[string]$Expected,
[string]$Context
)

if ($Actual -ne $Expected) {
throw "${Context}: expected [$Expected], actual [$Actual]."
}
}

function Invoke-WindowsPowerShellNativeArgumentRoundTrip {
param(
[string]$Argument
)

[string]$childScriptPath = Join-Path ([System.IO.Path]::GetTempPath()) ("uloop-native-argument-round-trip-" + [Guid]::NewGuid().ToString("N") + ".ps1")
Set-Content -LiteralPath $childScriptPath -Value '[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($args[0])))' -Encoding UTF8
[string[]]$childArguments = @("-NoProfile", "-File", $childScriptPath, $Argument)
[string[]]$escapedArguments = @($childArguments | ForEach-Object {
ConvertTo-WindowsPowerShellNativeArgument -Argument $_
})
[string]$encodedOutput = & powershell.exe @escapedArguments
if ($LASTEXITCODE -ne 0) {
Remove-Item -LiteralPath $childScriptPath
throw "Windows PowerShell native argument round trip failed."
}

Remove-Item -LiteralPath $childScriptPath
return [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encodedOutput))
}

[string[]]$definitions = @($ScriptPaths | ForEach-Object {
Get-FunctionDefinition -ScriptPath $_ -FunctionName "ConvertTo-WindowsPowerShellNativeArgument"
})

foreach ($definition in @($definitions | Select-Object -Skip 1)) {
Assert-Equal -Actual $definition -Expected $definitions[0] -Context "All E2E scripts must use the same native argument escaping implementation"
}

[string]$temporaryFunctionPath = Join-Path ([System.IO.Path]::GetTempPath()) "uloop-native-argument-escaping.ps1"
Set-Content -LiteralPath $temporaryFunctionPath -Value $definitions[0] -Encoding UTF8
. $temporaryFunctionPath
Remove-Item -LiteralPath $temporaryFunctionPath

Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\"') -Expected '\\\"' -Context "Existing escaped quote"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example Name\') -Expected 'C:\Users\Example Name\\' -Context "Trailing backslash in quoted argument"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\\\"') -Expected '\\\\\\\"' -Context "Consecutive backslashes before quote"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example\Project') -Expected 'C:\Users\Example\Project' -Context "Ordinary Windows path"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '日本語\パス"値') -Expected '日本語\パス\"値' -Context "Japanese text"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument 'C:\Users\Example\') -Expected 'C:\Users\Example\' -Context "Unquoted path with trailing backslash"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument '\') -Expected '\' -Context "Unquoted standalone backslash"
Assert-Equal -Actual (ConvertTo-WindowsPowerShellNativeArgument -Argument "C:\Users\Example\`n") -Expected "C:\Users\Example\`n" -Context "Trailing backslash before line feed"
if ($PSVersionTable.PSVersion.Major -lt 6) {
Assert-Equal -Actual (Invoke-WindowsPowerShellNativeArgumentRoundTrip -Argument 'C:\Users\Example\') -Expected 'C:\Users\Example\' -Context "Native process receives unquoted path with trailing backslash"
Assert-Equal -Actual (Invoke-WindowsPowerShellNativeArgumentRoundTrip -Argument 'C:\Users\Example Name\') -Expected 'C:\Users\Example Name\' -Context "Native process receives quoted path with trailing backslash"
}

Write-Host "PowerShell native argument escaping tests passed."
29 changes: 27 additions & 2 deletions scripts/test-simulate-mouse-demo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,40 @@ function Get-UloopArguments {
return @($CommandArguments + @("--project-path", $ProjectPath))
}

function ConvertTo-WindowsPowerShellNativeArgument {
param(
[string]$Argument
)

# Windows argv rules treat backslashes specially only before a quote or the closing quote.
# Do not double every backslash: ordinary paths such as C:\Users must stay unchanged.
[string]$escapedArgument = [regex]::Replace($Argument, '(\\*)"', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes + '\"'
})

if ($Argument -notmatch '\s') {
return $escapedArgument
}

return [regex]::Replace($escapedArgument, '(\\*)\z', {
param($match)

[string]$backslashes = $match.Groups[1].Value
return $backslashes + $backslashes
})
}

function Invoke-UloopCapture {
param(
[string[]]$CommandArguments
)

[string[]]$arguments = Get-UloopArguments -CommandArguments $CommandArguments
if ($PSVersionTable.PSVersion.Major -lt 6) {
# Windows PowerShell strips embedded quote characters from native arguments unless they are escaped.
$arguments = @($arguments | ForEach-Object { $_.Replace('"', '\"') })
$arguments = @($arguments | ForEach-Object { ConvertTo-WindowsPowerShellNativeArgument -Argument $_ })
}

[string]$previousErrorActionPreference = $ErrorActionPreference
Expand Down