Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows argv to string module utility #28970

Merged
merged 2 commits into from
Oct 22, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

# The rules used in these functions are derived from the below
# https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments
# https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/

Function Escape-Argument($argument, $force_quote=$false) {
# this converts a single argument to an escaped version, use Join-Arguments
# instead of this function as this only escapes a single string.

# check if argument contains a space, \n, \t, \v or "
if ($force_quote -eq $false -and $argument.Length -gt 0 -and $argument -notmatch "[ \n\t\v`"]") {
# argument does not need escaping (and we don't want to force it),
# return as is
return $argument
} else {
# we need to quote the arg so start with "
$new_argument = '"'

for ($i = 0; $i -lt $argument.Length; $i++) {
$num_backslashes = 0

# get the number of \ from current char until end or not a \
while ($i -ne ($argument.Length - 1) -and $argument[$i] -eq "\") {
$num_backslashes++
$i++
}

$current_char = $argument[$i]
if ($i -eq ($argument.Length -1) -and $current_char -eq "\") {
# We are at the end of the string so we need to add the same \
# * 2 as the end char would be a "
$new_argument += ("\" * ($num_backslashes + 1) * 2)
} elseif ($current_char -eq '"') {
# we have a inline ", we need to add the existing \ but * by 2
# plus another 1
$new_argument += ("\" * (($num_backslashes * 2) + 1))
$new_argument += $current_char
} else {
# normal character so no need to escape the \ we have counted
$new_argument += ("\" * $num_backslashes)
$new_argument += $current_char
}
}

# we need to close the special arg with a "
$new_argument += '"'
return $new_argument
}
}

Function Argv-ToString($arguments, $force_quote=$false) {
# Takes in a list of un escaped arguments and convert it to a single string
# that can be used when starting a new process. It will escape the
# characters as necessary in the list.
# While there is a CommandLineToArgvW function there is a no
# ArgvToCommandLineW that we can call to convert a list to an escaped
# string.
# You can also pass in force_quote so that each argument is quoted even
# when not necessary, by default only arguments with certain characters are
# quoted.
# TODO: add in another switch which will escape the args for cmd.exe

$escaped_arguments = @()
foreach ($argument in $arguments) {
$escaped_argument = Escape-Argument -argument $argument -force_quote $force_quote
$escaped_arguments += $escaped_argument
}

return ($escaped_arguments -join ' ')
}

# this line must stay at the bottom to ensure all defined module parts are exported
Export-ModuleMember -Alias * -Function * -Cmdlet *
13 changes: 13 additions & 0 deletions test/integration/targets/win_module_utils/files/PrintArgv.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
// This has been compiled to an exe and uploaded to S3 bucket for argv test

namespace PrintArgv
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(string.Join(System.Environment.NewLine, args));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!powershell

#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.ArgvParser

$ErrorActionPreference = 'Continue'

$params = Parse-Args $args
$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true

Add-Type -TypeDefinition @'
using System.IO;
using System.Threading;

namespace Ansible.Command
{
public static class NativeUtil
{
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
{
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
string so = null, se = null;
ThreadPool.QueueUserWorkItem((s)=>
{
so = stdoutStream.ReadToEnd();
sowait.Set();
});
ThreadPool.QueueUserWorkItem((s) =>
{
se = stderrStream.ReadToEnd();
sewait.Set();
});
foreach(var wh in new WaitHandle[] { sowait, sewait })
wh.WaitOne();
stdout = so;
stderr = se;
}
}
}
'@

Function Run-Process($executable, $arguments) {
$proc = New-Object System.Diagnostics.Process
$psi = $proc.StartInfo
$psi.FileName = $executable
$psi.Arguments = $arguments
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false

$proc.Start() | Out-Null # will always return $true for non shell-exec cases
$stdout = $stderr = [string] $null

[Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null
$proc.WaitForExit() | Out-Null
$actual_args = $stdout.Substring(0, $stdout.Length - 2) -split "`r`n"

return $actual_args
}

$tests = @(
@('abc', 'd', 'e'),
@('a\\b', 'de fg', 'h'),
@('a\"b', 'c', 'd'),
@('a\\b c', 'd', 'e'),
@('C:\Program Files\file\', 'arg with " quote'),
@('ADDLOCAL="a,b,c"', '/s', 'C:\\Double\\Backslash')
)

foreach ($expected in $tests) {
$joined_string = Argv-ToString -arguments $expected
# We can't used CommandLineToArgvW to test this out as it seems to mangle
# \, might be something to do with unicode but not sure...
$actual = Run-Process -executable $exe -arguments $joined_string

if ($expected.Count -ne $actual.Count) {
$result.actual = $actual -join "`n"
$result.expected = $expected -join "`n"
Fail-Json -obj $result -message "Actual arg count: $($actual.Count) != Expected arg count: $($expected.Count)"
}
for ($i = 0; $i -lt $expected.Count; $i++) {
$expected_arg = $expected[$i]
$actual_arg = $actual[$i]
if ($expected_arg -cne $actual_arg) {
$result.actual = $actual -join "`n"
$result.expected = $expected -join "`n"
Fail-Json -obj $result -message "Actual arg: '$actual_arg' != Expected arg: '$expected_arg'"
}
}
}

Exit-Json @{ data = 'success' }
11 changes: 10 additions & 1 deletion test/integration/targets/win_module_utils/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
that:
- sid_test.data == 'success'

- name: create testing folder for argv binary
- name: create temp testing folder
win_file:
path: C:\ansible testing
state: directory
Expand All @@ -67,6 +67,15 @@
that:
- command_util.data == 'success'

- name: call module with ArgvParser tests
argv_parser_test:
exe: C:\ansible testing\PrintArgv.exe
register: argv_test

- assert:
that:
- argv_test.data == 'success'

- name: remove testing folder
win_file:
path: C:\ansible testing
Expand Down