From b663f602bce941bedd432c992d0011cc59ed49ea Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Mon, 23 Oct 2017 09:49:40 +1000 Subject: [PATCH] windows argv to string module utility (#28970) * windows argv to string module utility * rebased PR with latest devel branch --- .../Ansible.ModuleUtils.ArgvParser.psm1 | 75 +++++++++++++++ .../win_module_utils/files/PrintArgv.cs | 13 +++ .../library/argv_parser_test.ps1 | 93 +++++++++++++++++++ .../targets/win_module_utils/tasks/main.yml | 11 ++- 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 create mode 100644 test/integration/targets/win_module_utils/files/PrintArgv.cs create mode 100644 test/integration/targets/win_module_utils/library/argv_parser_test.ps1 diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 new file mode 100644 index 00000000000..26d1e82a694 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 @@ -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 * diff --git a/test/integration/targets/win_module_utils/files/PrintArgv.cs b/test/integration/targets/win_module_utils/files/PrintArgv.cs new file mode 100644 index 00000000000..5ca3a8a01d5 --- /dev/null +++ b/test/integration/targets/win_module_utils/files/PrintArgv.cs @@ -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)); + } + } +} diff --git a/test/integration/targets/win_module_utils/library/argv_parser_test.ps1 b/test/integration/targets/win_module_utils/library/argv_parser_test.ps1 new file mode 100644 index 00000000000..6bcd52f2cf7 --- /dev/null +++ b/test/integration/targets/win_module_utils/library/argv_parser_test.ps1 @@ -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' } diff --git a/test/integration/targets/win_module_utils/tasks/main.yml b/test/integration/targets/win_module_utils/tasks/main.yml index baf43177fca..f00f08e1e9a 100644 --- a/test/integration/targets/win_module_utils/tasks/main.yml +++ b/test/integration/targets/win_module_utils/tasks/main.yml @@ -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 @@ -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