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