Skip to content
Open
68 changes: 46 additions & 22 deletions app-runner/Private/AndroidHelpers.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,45 +39,69 @@ function ConvertFrom-AndroidActivityPath {

<#
.SYNOPSIS
Validates that Android Intent extras are in the correct format.
Validates that an array of arguments can be safely converted to Intent extras format.

.DESCRIPTION
Android Intent extras should be passed in the format understood by `am start`.
This function validates and optionally formats the arguments string.

Common Intent extra formats:
-e key value String extra
-es key value String extra (explicit)
-ez key true|false Boolean extra
-ei key value Integer extra
-el key value Long extra
Validates each element of an argument array to ensure they form valid Intent extras
when combined. This prevents issues where individual elements are valid but the
combined string breaks Intent extras format.

.PARAMETER Arguments
The arguments string to validate/format
Array of string arguments to validate

.EXAMPLE
Test-IntentExtrasFormat "-e cmdline -crash-capture"
Test-IntentExtrasArray @('-e', 'key', 'value')
Returns: $true

.EXAMPLE
Test-IntentExtrasFormat "-e test true -ez debug false"
Returns: $true
Test-IntentExtrasArray @('-e', 'key with spaces', 'value')
Returns: $true (will be quoted properly)

.EXAMPLE
Test-IntentExtrasArray @('invalid', 'format')
Throws error for invalid format
#>
function Test-IntentExtrasFormat {
function Test-IntentExtrasArray {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$Arguments
[string[]]$Arguments
)

if ([string]::IsNullOrWhiteSpace($Arguments)) {
if (-not $Arguments -or $Arguments.Count -eq 0) {
return $true
}

# Intent extras must start with flags: -e, -es, -ez, -ei, -el, -ef, -eu, etc.
# Followed by at least one whitespace and additional content
if ($Arguments -notmatch '^--?[a-z]{1,2}\s+') {
throw "Invalid Intent extras format: '$Arguments'. Must start with flags like -e, -es, -ez, -ei, -el, etc. followed by key-value pairs."
# Only validate common Intent extras flags that we understand
# Ignore unknown flags to avoid breaking when Android adds new ones
$knownFlags = @('-e', '-es', '--es', '-ez', '--ez', '-ei', '--ei', '-el', '--el')

$i = 0
while ($i -lt $Arguments.Count) {
$currentArg = $Arguments[$i]

# If this looks like a flag we know, validate its structure
if ($knownFlags -contains $currentArg) {
# Ensure we have at least 2 more arguments (key and value)
if ($i + 2 -ge $Arguments.Count) {
throw "Invalid Intent extras format: Flag '$currentArg' must be followed by key and value. Missing arguments."
}

$key = $Arguments[$i + 1]
$value = $Arguments[$i + 2]

# For boolean flags, validate the value
if ($currentArg -in @('-ez', '--ez') -and $value -notin @('true', 'false')) {
throw "Invalid Intent extras format: Boolean flag '$currentArg' requires 'true' or 'false' value, got: '$value'"
}

# Skip the key and value we just processed
$i += 3
} else {
# Unknown flag - skip it
# This allows other Android flags to work without breaking our validation
$i += 1
}
}

return $true
Expand Down Expand Up @@ -132,7 +156,7 @@ function Get-ApkPackageName {
}

Write-Debug "Using $($aaptCmd.Name) to extract package name from APK"

try {
$PSNativeCommandUseErrorActionPreference = $false
$output = & $aaptCmd.Name dump badging $ApkPath 2>&1
Expand Down
15 changes: 9 additions & 6 deletions app-runner/Private/DeviceProviders/AdbProvider.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ class AdbProvider : DeviceProvider {
}
}

[hashtable] RunApplication([string]$ExecutablePath, [string]$Arguments) {
[hashtable] RunApplication([string]$ExecutablePath, [string[]]$Arguments, [string]$LogFilePath = $null) {
# LogFilePath parameter ignored in this implementation
Write-Debug "$($this.Platform): Running application: $ExecutablePath"

# Parse ExecutablePath: "package.name/activity.name"
Expand All @@ -205,8 +206,8 @@ class AdbProvider : DeviceProvider {
$this.CurrentPackageName = $packageName

# Validate Intent extras format
if ($Arguments) {
Test-IntentExtrasFormat -Arguments $Arguments | Out-Null
if ($Arguments -and $Arguments.Count -gt 0) {
Test-IntentExtrasArray -Arguments $Arguments | Out-Null
}

$timeoutSeconds = $this.Timeouts['run-timeout']
Expand All @@ -221,11 +222,13 @@ class AdbProvider : DeviceProvider {

# Launch activity
Write-Host "Launching: $ExecutablePath" -ForegroundColor Cyan
if ($Arguments) {
Write-Host " Arguments: $Arguments" -ForegroundColor Cyan

$argumentsString = $this.ConvertArgumentsToString($Arguments)
if ($argumentsString) {
Write-Host " Arguments: $argumentsString" -ForegroundColor Cyan
}

$launchOutput = $this.InvokeCommand('launch', @($this.DeviceSerial, $ExecutablePath, $Arguments))
$launchOutput = $this.InvokeCommand('launch', @($this.DeviceSerial, $ExecutablePath, $argumentsString))

# Join output to string first since -match on arrays returns matching elements, not boolean
if (($launchOutput -join "`n") -match 'Error') {
Expand Down
34 changes: 31 additions & 3 deletions app-runner/Private/DeviceProviders/DeviceProvider.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ class DeviceProvider {
return [BuiltCommand]::new($command, $processingCommand)
}

# Helper method to convert string array arguments to properly formatted string
# Joins with spaces and quotes arguments containing spaces or special characters
# Preserves arguments that are already quoted with matching quotes
[string] ConvertArgumentsToString([string[]]$Arguments) {
if (-not $Arguments -or $Arguments.Count -eq 0) {
return ""
}

$formattedArgs = @()

foreach ($arg in $Arguments) {
# If argument is already quoted with matching quotes, preserve it as-is
if ($arg.Length -ge 2 -and (($arg[0] -eq '"' -and $arg[-1] -eq '"') -or ($arg[0] -eq "'" -and $arg[-1] -eq "'"))) {
# Preserve original formatting for already-quoted arguments because
# the argument was intentionally quoted by the caller for a specific reason
$formattedArgs += $arg
} elseif ($arg -match '[\s"''&|<>^]' -or $arg -eq '--') {
# Escape single quotes with shell-compatible escaping
$escapedArg = $arg -replace "'", "'\''"
$formattedArgs += "'" + $escapedArg + "'"
} else {
$formattedArgs += $arg
}
}

return $formattedArgs -join ' '
}

[void] LogNotImplemented([string]$operation) {
Write-Warning "$($this.Platform) $operation not yet implemented"
Expand Down Expand Up @@ -372,14 +399,15 @@ class DeviceProvider {
return @{}
}

[hashtable] RunApplication([string]$ExecutablePath, [string]$Arguments) {
[hashtable] RunApplication([string]$ExecutablePath, [string[]]$Arguments, [string]$LogFilePath = $null) {
Write-Debug "$($this.Platform): Running application: $ExecutablePath with arguments: $Arguments"

$command = $this.BuildCommand('launch', @($ExecutablePath, $Arguments))
$argumentsString = $this.ConvertArgumentsToString($Arguments)
$command = $this.BuildCommand('launch', @($ExecutablePath, $argumentsString))
return $this.InvokeApplicationCommand($command, $ExecutablePath, $Arguments)
}

[hashtable] InvokeApplicationCommand([BuiltCommand]$builtCommand, [string]$ExecutablePath, [string]$Arguments) {
[hashtable] InvokeApplicationCommand([BuiltCommand]$builtCommand, [string]$ExecutablePath, [string[]]$Arguments) {
Write-Debug "$($this.Platform): Invoking $($builtCommand.Command)"

$result = $null
Expand Down
2 changes: 1 addition & 1 deletion app-runner/Private/DeviceProviders/MockDeviceProvider.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class MockDeviceProvider : DeviceProvider {
}
}

[hashtable] RunApplication([string]$ExecutablePath, [string]$Arguments) {
[hashtable] RunApplication([string]$ExecutablePath, [string[]]$Arguments, [string]$LogFilePath = $null) {
Write-Debug "Mock: Running application $ExecutablePath with args: $Arguments"

$this.MockConfig.AppRunning = $true
Expand Down
Loading
Loading