1 # Copyright (c) 2017 Ansible Project
2 # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
3
4 #Requires -Module Ansible.ModuleUtils.PrivilegeUtil
5
Load-LinkUtils()6 Function Load-LinkUtils() {
7 $link_util = @'
8 using Microsoft.Win32.SafeHandles;
9 using System;
10 using System.Collections.Generic;
11 using System.IO;
12 using System.Runtime.InteropServices;
13 using System.Text;
14
15 namespace Ansible
16 {
17 public enum LinkType
18 {
19 SymbolicLink,
20 JunctionPoint,
21 HardLink
22 }
23
24 public class LinkUtilWin32Exception : System.ComponentModel.Win32Exception
25 {
26 private string _msg;
27
28 public LinkUtilWin32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
29
30 public LinkUtilWin32Exception(int errorCode, string message) : base(errorCode)
31 {
32 _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
33 }
34
35 public override string Message { get { return _msg; } }
36 public static explicit operator LinkUtilWin32Exception(string message) { return new LinkUtilWin32Exception(message); }
37 }
38
39 public class LinkInfo
40 {
41 public LinkType Type { get; internal set; }
42 public string PrintName { get; internal set; }
43 public string SubstituteName { get; internal set; }
44 public string AbsolutePath { get; internal set; }
45 public string TargetPath { get; internal set; }
46 public string[] HardTargets { get; internal set; }
47 }
48
49 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
50 public struct REPARSE_DATA_BUFFER
51 {
52 public UInt32 ReparseTag;
53 public UInt16 ReparseDataLength;
54 public UInt16 Reserved;
55 public UInt16 SubstituteNameOffset;
56 public UInt16 SubstituteNameLength;
57 public UInt16 PrintNameOffset;
58 public UInt16 PrintNameLength;
59
60 [MarshalAs(UnmanagedType.ByValArray, SizeConst = LinkUtil.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)]
61 public char[] PathBuffer;
62 }
63
64 public class LinkUtil
65 {
66 public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 1024 * 16;
67
68 private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
69 private const UInt32 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
70
71 private const UInt32 FSCTL_GET_REPARSE_POINT = 0x000900A8;
72 private const UInt32 FSCTL_SET_REPARSE_POINT = 0x000900A4;
73 private const UInt32 FILE_DEVICE_FILE_SYSTEM = 0x00090000;
74
75 private const UInt32 IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
76 private const UInt32 IO_REPARSE_TAG_SYMLINK = 0xA000000C;
77
78 private const UInt32 SYMLINK_FLAG_RELATIVE = 0x00000001;
79
80 private const Int64 INVALID_HANDLE_VALUE = -1;
81
82 private const UInt32 SIZE_OF_WCHAR = 2;
83
84 private const UInt32 SYMBOLIC_LINK_FLAG_FILE = 0x00000000;
85 private const UInt32 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x00000001;
86
87 [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
88 private static extern SafeFileHandle CreateFile(
89 string lpFileName,
90 [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
91 [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
92 IntPtr lpSecurityAttributes,
93 [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
94 UInt32 dwFlagsAndAttributes,
95 IntPtr hTemplateFile);
96
97 // Used by GetReparsePointInfo()
98 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
99 private static extern bool DeviceIoControl(
100 SafeFileHandle hDevice,
101 UInt32 dwIoControlCode,
102 IntPtr lpInBuffer,
103 UInt32 nInBufferSize,
104 out REPARSE_DATA_BUFFER lpOutBuffer,
105 UInt32 nOutBufferSize,
106 out UInt32 lpBytesReturned,
107 IntPtr lpOverlapped);
108
109 // Used by CreateJunctionPoint()
110 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
111 private static extern bool DeviceIoControl(
112 SafeFileHandle hDevice,
113 UInt32 dwIoControlCode,
114 REPARSE_DATA_BUFFER lpInBuffer,
115 UInt32 nInBufferSize,
116 IntPtr lpOutBuffer,
117 UInt32 nOutBufferSize,
118 out UInt32 lpBytesReturned,
119 IntPtr lpOverlapped);
120
121 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
122 private static extern bool GetVolumePathName(
123 string lpszFileName,
124 StringBuilder lpszVolumePathName,
125 ref UInt32 cchBufferLength);
126
127 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
128 private static extern IntPtr FindFirstFileNameW(
129 string lpFileName,
130 UInt32 dwFlags,
131 ref UInt32 StringLength,
132 StringBuilder LinkName);
133
134 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
135 private static extern bool FindNextFileNameW(
136 IntPtr hFindStream,
137 ref UInt32 StringLength,
138 StringBuilder LinkName);
139
140 [DllImport("kernel32.dll", SetLastError = true)]
141 private static extern bool FindClose(
142 IntPtr hFindFile);
143
144 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
145 private static extern bool RemoveDirectory(
146 string lpPathName);
147
148 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
149 private static extern bool DeleteFile(
150 string lpFileName);
151
152 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
153 private static extern bool CreateSymbolicLink(
154 string lpSymlinkFileName,
155 string lpTargetFileName,
156 UInt32 dwFlags);
157
158 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
159 private static extern bool CreateHardLink(
160 string lpFileName,
161 string lpExistingFileName,
162 IntPtr lpSecurityAttributes);
163
164 public static LinkInfo GetLinkInfo(string linkPath)
165 {
166 FileAttributes attr = File.GetAttributes(linkPath);
167 if (attr.HasFlag(FileAttributes.ReparsePoint))
168 return GetReparsePointInfo(linkPath);
169
170 if (!attr.HasFlag(FileAttributes.Directory))
171 return GetHardLinkInfo(linkPath);
172
173 return null;
174 }
175
176 public static void DeleteLink(string linkPath)
177 {
178 bool success;
179 FileAttributes attr = File.GetAttributes(linkPath);
180 if (attr.HasFlag(FileAttributes.Directory))
181 {
182 success = RemoveDirectory(linkPath);
183 }
184 else
185 {
186 success = DeleteFile(linkPath);
187 }
188
189 if (!success)
190 throw new LinkUtilWin32Exception(String.Format("Failed to delete link at {0}", linkPath));
191 }
192
193 public static void CreateLink(string linkPath, String linkTarget, LinkType linkType)
194 {
195 switch (linkType)
196 {
197 case LinkType.SymbolicLink:
198 UInt32 linkFlags;
199 FileAttributes attr = File.GetAttributes(linkTarget);
200 if (attr.HasFlag(FileAttributes.Directory))
201 linkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY;
202 else
203 linkFlags = SYMBOLIC_LINK_FLAG_FILE;
204
205 if (!CreateSymbolicLink(linkPath, linkTarget, linkFlags))
206 throw new LinkUtilWin32Exception(String.Format("CreateSymbolicLink({0}, {1}, {2}) failed", linkPath, linkTarget, linkFlags));
207 break;
208 case LinkType.JunctionPoint:
209 CreateJunctionPoint(linkPath, linkTarget);
210 break;
211 case LinkType.HardLink:
212 if (!CreateHardLink(linkPath, linkTarget, IntPtr.Zero))
213 throw new LinkUtilWin32Exception(String.Format("CreateHardLink({0}, {1}) failed", linkPath, linkTarget));
214 break;
215 }
216 }
217
218 private static LinkInfo GetHardLinkInfo(string linkPath)
219 {
220 UInt32 maxPath = 260;
221 List<string> result = new List<string>();
222
223 StringBuilder sb = new StringBuilder((int)maxPath);
224 UInt32 stringLength = maxPath;
225 if (!GetVolumePathName(linkPath, sb, ref stringLength))
226 throw new LinkUtilWin32Exception("GetVolumePathName() failed");
227 string volume = sb.ToString();
228
229 stringLength = maxPath;
230 IntPtr findHandle = FindFirstFileNameW(linkPath, 0, ref stringLength, sb);
231 if (findHandle.ToInt64() != INVALID_HANDLE_VALUE)
232 {
233 try
234 {
235 do
236 {
237 string hardLinkPath = sb.ToString();
238 if (hardLinkPath.StartsWith("\\"))
239 hardLinkPath = hardLinkPath.Substring(1, hardLinkPath.Length - 1);
240
241 result.Add(Path.Combine(volume, hardLinkPath));
242 stringLength = maxPath;
243
244 } while (FindNextFileNameW(findHandle, ref stringLength, sb));
245 }
246 finally
247 {
248 FindClose(findHandle);
249 }
250 }
251
252 if (result.Count > 1)
253 return new LinkInfo
254 {
255 Type = LinkType.HardLink,
256 HardTargets = result.ToArray()
257 };
258
259 return null;
260 }
261
262 private static LinkInfo GetReparsePointInfo(string linkPath)
263 {
264 SafeFileHandle fileHandle = CreateFile(
265 linkPath,
266 FileAccess.Read,
267 FileShare.None,
268 IntPtr.Zero,
269 FileMode.Open,
270 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
271 IntPtr.Zero);
272
273 if (fileHandle.IsInvalid)
274 throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
275
276 REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
277 UInt32 bytesReturned;
278 try
279 {
280 if (!DeviceIoControl(
281 fileHandle,
282 FSCTL_GET_REPARSE_POINT,
283 IntPtr.Zero,
284 0,
285 out buffer,
286 MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
287 out bytesReturned,
288 IntPtr.Zero))
289 throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed for file at {0}", linkPath));
290 }
291 finally
292 {
293 fileHandle.Dispose();
294 }
295
296 bool isRelative = false;
297 int pathOffset = 0;
298 LinkType linkType;
299 if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
300 {
301 UInt32 bufferFlags = Convert.ToUInt32(buffer.PathBuffer[0]) + Convert.ToUInt32(buffer.PathBuffer[1]);
302 if (bufferFlags == SYMLINK_FLAG_RELATIVE)
303 isRelative = true;
304 pathOffset = 2;
305 linkType = LinkType.SymbolicLink;
306 }
307 else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
308 {
309 linkType = LinkType.JunctionPoint;
310 }
311 else
312 {
313 string errorMessage = String.Format("Invalid Reparse Tag: {0}", buffer.ReparseTag.ToString());
314 throw new Exception(errorMessage);
315 }
316
317 string printName = new string(buffer.PathBuffer, (int)(buffer.PrintNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.PrintNameLength / SIZE_OF_WCHAR));
318 string substituteName = new string(buffer.PathBuffer, (int)(buffer.SubstituteNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.SubstituteNameLength / SIZE_OF_WCHAR));
319
320 // TODO: should we check for \?\UNC\server for convert it to the NT style \\server path
321 // Remove the leading Windows object directory \?\ from the path if present
322 string targetPath = substituteName;
323 if (targetPath.StartsWith("\\??\\"))
324 targetPath = targetPath.Substring(4, targetPath.Length - 4);
325
326 string absolutePath = targetPath;
327 if (isRelative)
328 absolutePath = Path.GetFullPath(Path.Combine(new FileInfo(linkPath).Directory.FullName, targetPath));
329
330 return new LinkInfo
331 {
332 Type = linkType,
333 PrintName = printName,
334 SubstituteName = substituteName,
335 AbsolutePath = absolutePath,
336 TargetPath = targetPath
337 };
338 }
339
340 private static void CreateJunctionPoint(string linkPath, string linkTarget)
341 {
342 // We need to create the link as a dir beforehand
343 Directory.CreateDirectory(linkPath);
344 SafeFileHandle fileHandle = CreateFile(
345 linkPath,
346 FileAccess.Write,
347 FileShare.Read | FileShare.Write | FileShare.None,
348 IntPtr.Zero,
349 FileMode.Open,
350 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
351 IntPtr.Zero);
352
353 if (fileHandle.IsInvalid)
354 throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
355
356 try
357 {
358 string substituteName = "\\??\\" + Path.GetFullPath(linkTarget);
359 string printName = linkTarget;
360
361 REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
362 buffer.SubstituteNameOffset = 0;
363 buffer.SubstituteNameLength = (UInt16)(substituteName.Length * SIZE_OF_WCHAR);
364 buffer.PrintNameOffset = (UInt16)(buffer.SubstituteNameLength + 2);
365 buffer.PrintNameLength = (UInt16)(printName.Length * SIZE_OF_WCHAR);
366
367 buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
368 buffer.ReparseDataLength = (UInt16)(buffer.SubstituteNameLength + buffer.PrintNameLength + 12);
369 buffer.PathBuffer = new char[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
370
371 byte[] unicodeBytes = Encoding.Unicode.GetBytes(substituteName + "\0" + printName);
372 char[] pathBuffer = Encoding.Unicode.GetChars(unicodeBytes);
373 Array.Copy(pathBuffer, buffer.PathBuffer, pathBuffer.Length);
374
375 UInt32 bytesReturned;
376 if (!DeviceIoControl(
377 fileHandle,
378 FSCTL_SET_REPARSE_POINT,
379 buffer,
380 (UInt32)(buffer.ReparseDataLength + 8),
381 IntPtr.Zero, 0,
382 out bytesReturned,
383 IntPtr.Zero))
384 throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed to create junction point at {0} to {1}", linkPath, linkTarget));
385 }
386 finally
387 {
388 fileHandle.Dispose();
389 }
390 }
391 }
392 }
393 '@
394
395 # FUTURE: find a better way to get the _ansible_remote_tmp variable
396 $original_tmp = $env:TMP
397
398 $remote_tmp = $original_tmp
399 $module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue
400 if ($module_params) {
401 if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) {
402 $remote_tmp = $module_params.Value["_ansible_remote_tmp"]
403 $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
404 }
405 }
406
407 $env:TMP = $remote_tmp
408 Add-Type -TypeDefinition $link_util
409 $env:TMP = $original_tmp
410
411 # enable the SeBackupPrivilege if it is disabled
412 $state = Get-AnsiblePrivilege -Name SeBackupPrivilege
413 if ($state -eq $false) {
414 Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
415 }
416 }
417
Get-Link($link_path)418 Function Get-Link($link_path) {
419 $link_info = [Ansible.LinkUtil]::GetLinkInfo($link_path)
420 return $link_info
421 }
422
Remove-Link($link_path)423 Function Remove-Link($link_path) {
424 [Ansible.LinkUtil]::DeleteLink($link_path)
425 }
426
New-Link($link_path, $link_target, $link_type)427 Function New-Link($link_path, $link_target, $link_type) {
428 if (-not (Test-Path -LiteralPath $link_target)) {
429 throw "link_target '$link_target' does not exist, cannot create link"
430 }
431
432 switch($link_type) {
433 "link" {
434 $type = [Ansible.LinkType]::SymbolicLink
435 }
436 "junction" {
437 if (Test-Path -LiteralPath $link_target -PathType Leaf) {
438 throw "cannot set the target for a junction point to a file"
439 }
440 $type = [Ansible.LinkType]::JunctionPoint
441 }
442 "hard" {
443 if (Test-Path -LiteralPath $link_target -PathType Container) {
444 throw "cannot set the target for a hard link to a directory"
445 }
446 $type = [Ansible.LinkType]::HardLink
447 }
448 default { throw "invalid link_type option $($link_type): expecting link, junction, hard" }
449 }
450 [Ansible.LinkUtil]::CreateLink($link_path, $link_target, $type)
451 }
452
453 # this line must stay at the bottom to ensure all defined module parts are exported
454 Export-ModuleMember -Alias * -Function * -Cmdlet *
455