1 #!powershell
2 
3 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4 
5 #Requires -Module Ansible.ModuleUtils.Legacy
6 #Requires -Module Ansible.ModuleUtils.Backup
7 
WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode)8 function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) {
9 	Try {
10 		$temppath = [System.IO.Path]::GetTempFileName();
11 	}
12 	Catch {
13 		Fail-Json @{} "Cannot create temporary file! ($($_.Exception.Message))";
14 	}
15 	$joined = $outlines -join $linesep;
16 	[System.IO.File]::WriteAllText($temppath, $joined, $encodingobj);
17 
18 	If ($validate) {
19 
20 		If (-not ($validate -like "*%s*")) {
21 			Fail-Json @{} "validate must contain %s: $validate";
22 		}
23 
24 		$validate = $validate.Replace("%s", $temppath);
25 
26 		$parts = [System.Collections.ArrayList] $validate.Split(" ");
27 		$cmdname = $parts[0];
28 
29 		$cmdargs = $validate.Substring($cmdname.Length + 1);
30 
31 		$process = [Diagnostics.Process]::Start($cmdname, $cmdargs);
32 		$process.WaitForExit();
33 
34 		If ($process.ExitCode -ne 0) {
35 			[string] $output = $process.StandardOutput.ReadToEnd();
36 			[string] $error = $process.StandardError.ReadToEnd();
37 			Remove-Item $temppath -force;
38 			Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error";
39 		}
40 
41 	}
42 
43 	# Commit changes to the path
44 	$cleanpath = $path.Replace("/", "\");
45 	Try {
46 		Copy-Item -Path $temppath -Destination $cleanpath -Force -WhatIf:$check_mode;
47 	}
48 	Catch {
49 		Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))";
50 	}
51 
52 	Try {
53 		Remove-Item -Path $temppath -Force -WhatIf:$check_mode;
54 	}
55 	Catch {
56 		Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))";
57 	}
58 
59 	return $joined;
60 
61 }
62 
63 
64 # Implement the functionality for state == 'present'
Present($path, $regex, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support)65 function Present($path, $regex, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
66 
67 	# Note that we have to clean up the path because ansible wants to treat / and \ as
68 	# interchangeable in windows pathnames, but .NET framework internals do not support that.
69 	$cleanpath = $path.Replace("/", "\");
70 
71 	# Check if path exists. If it does not exist, either create it if create == "yes"
72 	# was specified or fail with a reasonable error message.
73 	If (-not (Test-Path -LiteralPath $path)) {
74 		If (-not $create) {
75 			Fail-Json @{} "Path $path does not exist !";
76 		}
77 		# Create new empty file, using the specified encoding to write correct BOM
78 		[System.IO.File]::WriteAllLines($cleanpath, "", $encodingobj);
79 	}
80 
81 	# Initialize result information
82 	$result = @{
83 		backup = "";
84 		changed = $false;
85 		msg = "";
86 	}
87 
88 	# Read the dest file lines using the indicated encoding into a mutable ArrayList.
89 	$before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj)
90 	If ($null -eq $before) {
91 		$lines = New-Object System.Collections.ArrayList;
92 	}
93 	Else {
94 		$lines = [System.Collections.ArrayList] $before;
95 	}
96 
97 	if ($diff_support) {
98 		$result.diff = @{
99 			before = $before -join $linesep;
100 		}
101 	}
102 
103 	# Compile the regex specified, if provided
104 	$mre = $null;
105 	If ($regex) {
106 		$mre = New-Object Regex $regex, 'Compiled';
107 	}
108 
109 	# Compile the regex for insertafter or insertbefore, if provided
110 	$insre = $null;
111 	If ($insertafter -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") {
112 		$insre = New-Object Regex $insertafter, 'Compiled';
113 	}
114 	ElseIf ($insertbefore -and $insertbefore -ne "BOF") {
115 		$insre = New-Object Regex $insertbefore, 'Compiled';
116 	}
117 
118 	# index[0] is the line num where regex has been found
119 	# index[1] is the line num where insertafter/insertbefore has been found
120 	$index = -1, -1;
121 	$lineno = 0;
122 
123 	# The latest match object and matched line
124 	$matched_line = "";
125 
126 	# Iterate through the lines in the file looking for matches
127 	Foreach ($cur_line in $lines) {
128 		If ($regex) {
129 			$m = $mre.Match($cur_line);
130 			$match_found = $m.Success;
131 			If ($match_found) {
132 				$matched_line = $cur_line;
133 			}
134 		}
135 		Else {
136 			$match_found = $line -ceq $cur_line;
137 		}
138 		If ($match_found) {
139 			$index[0] = $lineno;
140 		}
141 		ElseIf ($insre -and $insre.Match($cur_line).Success) {
142 			If ($insertafter) {
143 				$index[1] = $lineno + 1;
144 			}
145 			If ($insertbefore) {
146 				$index[1] = $lineno;
147 			}
148 		}
149 		$lineno = $lineno + 1;
150 	}
151 
152 	If ($index[0] -ne -1) {
153 		If ($backrefs) {
154 		    $new_line = [regex]::Replace($matched_line, $regex, $line);
155 		}
156 		Else {
157 			$new_line = $line;
158 		}
159 		If ($lines[$index[0]] -cne $new_line) {
160 			$lines[$index[0]] = $new_line;
161 			$result.changed = $true;
162 			$result.msg = "line replaced";
163 		}
164 	}
165 	ElseIf ($backrefs) {
166 		# No matches - no-op
167 	}
168 	ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") {
169 		$lines.Insert(0, $line);
170 		$result.changed = $true;
171 		$result.msg = "line added";
172 	}
173 	ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) {
174 		$lines.Add($line) > $null;
175 		$result.changed = $true;
176 		$result.msg = "line added";
177 	}
178 	Else {
179 		$lines.Insert($index[1], $line);
180 		$result.changed = $true;
181 		$result.msg = "line added";
182 	}
183 
184 	# Write changes to the path if changes were made
185 	If ($result.changed) {
186 
187 		# Write backup file if backup == "yes"
188 		If ($backup) {
189 			$result.backup_file = Backup-File -path $path -WhatIf:$check_mode
190 			# Ensure backward compatibility (deprecate in future)
191 			$result.backup = $result.backup_file
192 		}
193 
194 		$writelines_params = @{
195 			outlines = $lines
196 			path = $path
197 			linesep = $linesep
198 			encodingobj = $encodingobj
199 			validate = $validate
200 			check_mode = $check_mode
201 		}
202 		$after = WriteLines @writelines_params;
203 
204 		if ($diff_support) {
205 			$result.diff.after = $after;
206 		}
207 	}
208 
209 	$result.encoding = $encodingobj.WebName;
210 
211 	Exit-Json $result;
212 }
213 
214 
215 # Implement the functionality for state == 'absent'
Absent($path, $regex, $line, $backup, $validate, $encodingobj, $linesep, $check_mode, $diff_support)216 function Absent($path, $regex, $line, $backup, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
217 
218 	# Check if path exists. If it does not exist, fail with a reasonable error message.
219 	If (-not (Test-Path -LiteralPath $path)) {
220 		Fail-Json @{} "Path $path does not exist !";
221 	}
222 
223 	# Initialize result information
224 	$result = @{
225 		backup = "";
226 		changed = $false;
227 		msg = "";
228 	}
229 
230 	# Read the dest file lines using the indicated encoding into a mutable ArrayList. Note
231 	# that we have to clean up the path because ansible wants to treat / and \ as
232 	# interchangeable in windows pathnames, but .NET framework internals do not support that.
233 	$cleanpath = $path.Replace("/", "\");
234 	$before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj);
235 	If ($null -eq $before) {
236 		$lines = New-Object System.Collections.ArrayList;
237 	}
238 	Else {
239 		$lines = [System.Collections.ArrayList] $before;
240 	}
241 
242 	if ($diff_support) {
243 		$result.diff = @{
244 			before = $before -join $linesep;
245 		}
246 	}
247 
248 	# Compile the regex specified, if provided
249 	$cre = $null;
250 	If ($regex) {
251 		$cre = New-Object Regex $regex, 'Compiled';
252 	}
253 
254 	$found = New-Object System.Collections.ArrayList;
255 	$left = New-Object System.Collections.ArrayList;
256 
257 	Foreach ($cur_line in $lines) {
258 		If ($regex) {
259 			$m = $cre.Match($cur_line);
260 			$match_found = $m.Success;
261 		}
262 		Else {
263 			$match_found = $line -ceq $cur_line;
264 		}
265 		If ($match_found) {
266 			$found.Add($cur_line) > $null;
267 			$result.changed = $true;
268 		}
269 		Else {
270 			$left.Add($cur_line) > $null;
271 		}
272 	}
273 
274 	# Write changes to the path if changes were made
275 	If ($result.changed) {
276 
277 		# Write backup file if backup == "yes"
278 		If ($backup) {
279 			$result.backup_file = Backup-File -path $path -WhatIf:$check_mode
280 			# Ensure backward compatibility (deprecate in future)
281 			$result.backup = $result.backup_file
282 		}
283 
284 		$writelines_params = @{
285 			outlines = $left
286 			path = $path
287 			linesep = $linesep
288 			encodingobj = $encodingobj
289 			validate = $validate
290 			check_mode = $check_mode
291 		}
292 		$after = WriteLines @writelines_params;
293 
294 		if ($diff_support) {
295 			$result.diff.after = $after;
296 		}
297 	}
298 
299 	$result.encoding = $encodingobj.WebName;
300 	$result.found = $found.Count;
301 	$result.msg = "$($found.Count) line(s) removed";
302 
303 	Exit-Json $result;
304 }
305 
306 
307 # Parse the parameters file dropped by the Ansible machinery
308 $params = Parse-Args $args -supports_check_mode $true;
309 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false;
310 $diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false;
311 
312 # Initialize defaults for input parameters.
313 $path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","destfile","name";
314 $regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "regexp";
315 $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent";
316 $line = Get-AnsibleParam -obj $params -name "line" -type "str";
317 $backrefs = Get-AnsibleParam -obj $params -name "backrefs" -type "bool" -default $false;
318 $insertafter = Get-AnsibleParam -obj $params -name "insertafter" -type "str";
319 $insertbefore = Get-AnsibleParam -obj $params -name "insertbefore" -type "str";
320 $create = Get-AnsibleParam -obj $params -name "create" -type "bool" -default $false;
321 $backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false;
322 $validate = Get-AnsibleParam -obj $params -name "validate" -type "str";
323 $encoding = Get-AnsibleParam -obj $params -name "encoding" -type "str" -default "auto";
324 $newline = Get-AnsibleParam -obj $params -name "newline" -type "str" -default "windows" -validateset "unix","windows";
325 
326 # Fail if the path is not a file
327 If (Test-Path -LiteralPath $path -PathType "container") {
328 	Fail-Json @{} "Path $path is a directory";
329 }
330 
331 # Default to windows line separator - probably most common
332 $linesep = "`r`n"
333 If ($newline -eq "unix") {
334 	$linesep = "`n";
335 }
336 
337 # Figure out the proper encoding to use for reading / writing the target file.
338 
339 # The default encoding is UTF-8 without BOM
340 $encodingobj = [System.Text.UTF8Encoding] $false;
341 
342 # If an explicit encoding is specified, use that instead
343 If ($encoding -ne "auto") {
344 	$encodingobj = [System.Text.Encoding]::GetEncoding($encoding);
345 }
346 
347 # Otherwise see if we can determine the current encoding of the target file.
348 # If the file doesn't exist yet (create == 'yes') we use the default or
349 # explicitly specified encoding set above.
350 ElseIf (Test-Path -LiteralPath $path) {
351 
352 	# Get a sorted list of encodings with preambles, longest first
353 	$max_preamble_len = 0;
354 	$sortedlist = New-Object System.Collections.SortedList;
355 	Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) {
356 		$encoding = $encodinginfo.GetEncoding();
357 		$plen = $encoding.GetPreamble().Length;
358 		If ($plen -gt $max_preamble_len) {
359 			$max_preamble_len = $plen;
360 		}
361 		If ($plen -gt 0) {
362 			$sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding) > $null;
363 		}
364 	}
365 
366 	# Get the first N bytes from the file, where N is the max preamble length we saw
367 	[Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -LiteralPath $path;
368 
369 	# Iterate through the sorted encodings, looking for a full match.
370 	$found = $false;
371 	Foreach ($encoding in $sortedlist.GetValueList()) {
372 		$preamble = $encoding.GetPreamble();
373 		If ($preamble -and $bom) {
374 			Foreach ($i in 0..($preamble.Length - 1)) {
375 				If ($i -ge $bom.Length) {
376 					break;
377 				}
378 				If ($preamble[$i] -ne $bom[$i]) {
379 					break;
380 				}
381 				ElseIf ($i + 1 -eq $preamble.Length) {
382 					$encodingobj = $encoding;
383 					$found = $true;
384 				}
385 			}
386 			If ($found) {
387 				break;
388 			}
389 		}
390 	}
391 }
392 
393 
394 # Main dispatch - based on the value of 'state', perform argument validation and
395 # call the appropriate handler function.
396 If ($state -eq "present") {
397 
398 	If ($backrefs -and -not $regex) {
399 	    Fail-Json @{} "regexp= is required with backrefs=true";
400 	}
401 
402 	If (-not $line) {
403 		Fail-Json @{} "line= is required with state=present";
404 	}
405 
406 	If ($insertbefore -and $insertafter) {
407 		Add-Warning $result "Both insertbefore and insertafter parameters found, ignoring `"insertafter=$insertafter`""
408 	}
409 
410 	If (-not $insertbefore -and -not $insertafter) {
411 		$insertafter = "EOF";
412 	}
413 
414 	$present_params = @{
415 		path = $path
416 		regex = $regex
417 		line = $line
418 		insertafter = $insertafter
419 		insertbefore = $insertbefore
420 		create = $create
421 		backup = $backup
422 		backrefs = $backrefs
423 		validate = $validate
424 		encodingobj = $encodingobj
425 		linesep = $linesep
426 		check_mode = $check_mode
427 		diff_support = $diff_support
428 	}
429 	Present @present_params;
430 
431 }
432 ElseIf ($state -eq "absent") {
433 
434 	If (-not $regex -and -not $line) {
435 		Fail-Json @{} "one of line= or regexp= is required with state=absent";
436 	}
437 
438 	$absent_params = @{
439 		path = $path
440 		regex = $regex
441 		line = $line
442 		backup = $backup
443 		validate = $validate
444 		encodingobj = $encodingobj
445 		linesep = $linesep
446 		check_mode = $check_mode
447 		diff_support = $diff_support
448 	}
449 	Absent @absent_params;
450 }
451