Browse code

powershell - fix quoting values (#71411) (#71449)

* powershell - fix quoting values

* Add ignore for smart quote skip

(cherry picked from commit 72a7cb4a2c3036da5e3abb32c50713a262d0c063)

Jordan Borean authored on 2020/08/29 02:22:22
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+bugfixes:
1
+- powershell - fix escaping of strings that broken modules like fetch when dealing with special chars - https://github.com/ansible/ansible/issues/62781
... ...
@@ -649,7 +649,7 @@ class Connection(ConnectionBase):
649 649
             while True:
650 650
                 try:
651 651
                     script = '''
652
-                        $path = "%(path)s"
652
+                        $path = '%(path)s'
653 653
                         If (Test-Path -Path $path -PathType Leaf)
654 654
                         {
655 655
                             $buffer_size = %(buffer_size)d
... ...
@@ -120,9 +120,9 @@ class ShellModule(ShellBase):
120 120
     def remove(self, path, recurse=False):
121 121
         path = self._escape(self._unquote(path))
122 122
         if recurse:
123
-            return self._encode_script('''Remove-Item "%s" -Force -Recurse;''' % path)
123
+            return self._encode_script('''Remove-Item '%s' -Force -Recurse;''' % path)
124 124
         else:
125
-            return self._encode_script('''Remove-Item "%s" -Force;''' % path)
125
+            return self._encode_script('''Remove-Item '%s' -Force;''' % path)
126 126
 
127 127
     def mkdtemp(self, basefile=None, system=False, mode=None, tmpdir=None):
128 128
         # Windows does not have an equivalent for the system temp files, so
... ...
@@ -147,15 +147,15 @@ class ShellModule(ShellBase):
147 147
         if user_home_path == '~':
148 148
             script = 'Write-Output (Get-Location).Path'
149 149
         elif user_home_path.startswith('~\\'):
150
-            script = 'Write-Output ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:])
150
+            script = "Write-Output ((Get-Location).Path + '%s')" % self._escape(user_home_path[1:])
151 151
         else:
152
-            script = 'Write-Output "%s"' % self._escape(user_home_path)
152
+            script = "Write-Output '%s'" % self._escape(user_home_path)
153 153
         return self._encode_script(script)
154 154
 
155 155
     def exists(self, path):
156 156
         path = self._escape(self._unquote(path))
157 157
         script = '''
158
-            If (Test-Path "%s")
158
+            If (Test-Path '%s')
159 159
             {
160 160
                 $res = 0;
161 161
             }
... ...
@@ -163,7 +163,7 @@ class ShellModule(ShellBase):
163 163
             {
164 164
                 $res = 1;
165 165
             }
166
-            Write-Output "$res";
166
+            Write-Output '$res';
167 167
             Exit $res;
168 168
          ''' % path
169 169
         return self._encode_script(script)
... ...
@@ -171,14 +171,14 @@ class ShellModule(ShellBase):
171 171
     def checksum(self, path, *args, **kwargs):
172 172
         path = self._escape(self._unquote(path))
173 173
         script = '''
174
-            If (Test-Path -PathType Leaf "%(path)s")
174
+            If (Test-Path -PathType Leaf '%(path)s')
175 175
             {
176 176
                 $sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider;
177
-                $fp = [System.IO.File]::Open("%(path)s", [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read);
177
+                $fp = [System.IO.File]::Open('%(path)s', [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read);
178 178
                 [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
179 179
                 $fp.Dispose();
180 180
             }
181
-            ElseIf (Test-Path -PathType Container "%(path)s")
181
+            ElseIf (Test-Path -PathType Container '%(path)s')
182 182
             {
183 183
                 Write-Output "3";
184 184
             }
... ...
@@ -264,22 +264,11 @@ class ShellModule(ShellBase):
264 264
             return m.group(1)
265 265
         return value
266 266
 
267
-    def _escape(self, value, include_vars=False):
268
-        '''Return value escaped for use in PowerShell command.'''
269
-        # http://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences
270
-        # http://stackoverflow.com/questions/764360/a-list-of-string-replacements-in-python
271
-        subs = [('\n', '`n'), ('\r', '`r'), ('\t', '`t'), ('\a', '`a'),
272
-                ('\b', '`b'), ('\f', '`f'), ('\v', '`v'), ('"', '`"'),
273
-                ('\'', '`\''), ('`', '``'), ('\x00', '`0')]
274
-        if include_vars:
275
-            subs.append(('$', '`$'))
276
-        pattern = '|'.join('(%s)' % re.escape(p) for p, s in subs)
277
-        substs = [s for p, s in subs]
278
-
279
-        def replace(m):
280
-            return substs[m.lastindex - 1]
281
-
282
-        return re.sub(pattern, replace, value)
267
+    def _escape(self, value):
268
+        '''Return value escaped for use in PowerShell single quotes.'''
269
+        # There are 5 chars that need to be escaped in a single quote.
270
+        # https://github.com/PowerShell/PowerShell/blob/b7cb335f03fe2992d0cbd61699de9d9aafa1d7c1/src/System.Management.Automation/engine/parser/CharTraits.cs#L265-L272
271
+        return re.compile(u"(['\u2018\u2019\u201a\u201b])").sub(u'\\1\\1', value)
283 272
 
284 273
     def _encode_script(self, script, as_list=False, strict_mode=True, preserve_rc=True):
285 274
         '''Convert a PowerShell script to a single base64-encoded command.'''
286 275
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+dependencies:
1
+- setup_remote_tmp_dir
... ...
@@ -187,3 +187,26 @@
187 187
       # Doesn't fail anymore, only returns a message.
188 188
       - "fetch_dir is not changed"
189 189
       - "fetch_dir.msg"
190
+
191
+- name: create file with special characters
192
+  raw: Set-Content -LiteralPath '{{ remote_tmp_dir }}\abc$not var''quote‘‘' -Value 'abc'
193
+
194
+- name: fetch file with special characters
195
+  fetch:
196
+    src: '{{ remote_tmp_dir }}\abc$not var''quote‘'
197
+    dest: '{{ host_output_dir }}/'
198
+    flat: yes
199
+  register: fetch_special_file
200
+
201
+- name: get content of fetched file
202
+  command: cat {{ (host_output_dir ~ "/abc$not var'quote‘") | quote }}
203
+  register: fetch_special_file_actual
204
+  delegate_to: localhost
205
+
206
+- name: assert fetch file with special characters
207
+  assert:
208
+    that:
209
+    - fetch_special_file is changed
210
+    - fetch_special_file.checksum == '34d4150adc3347f1dd8ce19fdf65b74d971ab602'
211
+    - fetch_special_file.dest == host_output_dir + "/abc$not var'quote‘"
212
+    - fetch_special_file_actual.stdout == 'abc'
... ...
@@ -304,6 +304,7 @@ test/integration/targets/want_json_modules_posix/library/helloworld.py future-im
304 304
 test/integration/targets/want_json_modules_posix/library/helloworld.py metaclass-boilerplate
305 305
 test/integration/targets/win_exec_wrapper/library/test_fail.ps1 pslint:PSCustomUseLiteralPath
306 306
 test/integration/targets/win_exec_wrapper/tasks/main.yml no-smart-quotes  # We are explicitly testing smart quote support for env vars
307
+test/integration/targets/win_fetch/tasks/main.yml no-smart-quotes  # We are explictly testing smart quotes in the file name to fetch
307 308
 test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1 line-endings  # Explicitly tests that we still work with Windows line endings
308 309
 test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1 line-endings  # Explicitly tests that we still work with Windows line endings
309 310
 test/integration/targets/win_script/files/test_script.ps1 pslint:PSAvoidUsingWriteHost # Keep