1 #Region "Microsoft.VisualBasic::a0263259f674405a4fedec9c4e65ce60, Microsoft.VisualBasic.Core\Extensions\IO\SymLinker\JunctionPoint.vb"
2
3     ' Author:
4     
5     '       asuka (amethyst.asuka@gcmodeller.org)
6     '       xie (genetics@smrucc.org)
7     '       xieguigang (xie.guigang@live.com)
8     
9     ' Copyright (c) 2018 GPL3 Licensed
10     
11     
12     ' GNU GENERAL PUBLIC LICENSE (GPL3)
13     
14     
15     ' This program is free software: you can redistribute it and/or modify
16     ' it under the terms of the GNU General Public License as published by
17     ' the Free Software Foundation, either version 3 of the License, or
18     ' (at your option) any later version.
19     
20     ' This program is distributed in the hope that it will be useful,
21     ' but WITHOUT ANY WARRANTY; without even the implied warranty of
22     ' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23     ' GNU General Public License for more details.
24     
25     ' You should have received a copy of the GNU General Public License
26     ' along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28
29
30     ' /********************************************************************************/
31
32     ' Summaries:
33
34     '     Module JunctionPoint
35     
36     
37     '         Enum EFileAccess
38     
39     
40     
41     
42     '         Enum EFileShare
43     
44     
45     '  
46     
47     
48     
49     '         Enum ECreationDisposition
50     
51     
52     '  
53     
54     
55     
56     '         Enum EFileAttributes
57     
58     
59     '  
60     
61     
62     
63     '         Structure REPARSE_DATA_BUFFER
64     
65     
66     
67     '  
68     
69     '     Function: CreateFile, DeviceIoControl, Exists, GetTarget, InternalGetTarget
70     '               OpenReparsePoint
71     
72     '     Sub: Create, Delete, ThrowLastWin32Error
73     
74     
75     ' /********************************************************************************/
76
77 #End Region
78
79 Imports Microsoft.Win32.SafeHandles
80 Imports System.IO
81 Imports System.Runtime.InteropServices
82 Imports System.Text
83
84 Namespace FileIO.SymLinker
85
86     ''' <summary>
87     ''' Provides access to NTFS junction points in .Net.
88     ''' </summary>
89     Public Module JunctionPoint
90
91         ''' <summary>
92         ''' The file or directory is not a reparse point.
93         ''' </summary>
94         Private Const ERROR_NOT_A_REPARSE_POINT As Integer = 4390
95
96         ''' <summary>
97         ''' The reparse point attribute cannot be set because it conflicts with an existing attribute.
98         ''' </summary>
99         Private Const ERROR_REPARSE_ATTRIBUTE_CONFLICT As Integer = 4391
100
101         ''' <summary>
102         ''' The data present in the reparse point buffer is invalid.
103         ''' </summary>
104         Private Const ERROR_INVALID_REPARSE_DATA As Integer = 4392
105
106         ''' <summary>
107         ''' The tag present in the reparse point buffer is invalid.
108         ''' </summary>
109         Private Const ERROR_REPARSE_TAG_INVALID As Integer = 4393
110
111         ''' <summary>
112         ''' There is a mismatch between the tag specified in the request and the tag present in the reparse point.
113         ''' </summary>
114         Private Const ERROR_REPARSE_TAG_MISMATCH As Integer = 4394
115
116         ''' <summary>
117         ''' Command to set the reparse point data block.
118         ''' </summary>
119         Private Const FSCTL_SET_REPARSE_POINT As Integer = &H900A4
120
121         ''' <summary>
122         ''' Command to get the reparse point data block.
123         ''' </summary>
124         Private Const FSCTL_GET_REPARSE_POINT As Integer = &H900A8
125
126         ''' <summary>
127         ''' Command to delete the reparse point data base.
128         ''' </summary>
129         Private Const FSCTL_DELETE_REPARSE_POINT As Integer = &H900AC
130
131         ''' <summary>
132         ''' Reparse point tag used to identify mount points and junction points.
133         ''' </summary>
134         Private Const IO_REPARSE_TAG_MOUNT_POINT As UInteger = &HA0000003UI
135
136         ''' <summary>
137         ''' This prefix indicates to NTFS that the path is to be treated as a non-interpreted
138         ''' path in the virtual file system.
139         ''' </summary>
140         Private Const NonInterpretedPathPrefix As String = "\??\"
141
142         <Flags>
143         Private Enum EFileAccess As UInteger
144             GenericRead = &H80000000UI
145             GenericWrite = &H40000000
146             GenericExecute = &H20000000
147             GenericAll = &H10000000
148         End Enum
149
150         <Flags>
151         Private Enum EFileShare As UInteger
152             None = &H0
153             Read = &H1
154             Write = &H2
155             Delete = &H4
156         End Enum
157
158         Private Enum ECreationDisposition As UInteger
159             [New] = 1
160             CreateAlways = 2
161             OpenExisting = 3
162             OpenAlways = 4
163             TruncateExisting = 5
164         End Enum
165
166         <Flags>
167         Private Enum EFileAttributes As UInteger
168             [Readonly] = &H1
169             Hidden = &H2
170             System = &H4
171             Directory = &H10
172             Archive = &H20
173             Device = &H40
174             Normal = &H80
175             Temporary = &H100
176             SparseFile = &H200
177             ReparsePoint = &H400
178             Compressed = &H800
179             Offline = &H1000
180             NotContentIndexed = &H2000
181             Encrypted = &H4000
182             Write_Through = &H80000000UI
183             Overlapped = &H40000000
184             NoBuffering = &H20000000
185             RandomAccess = &H10000000
186             SequentialScan = &H8000000
187             DeleteOnClose = &H4000000
188             BackupSemantics = &H2000000
189             PosixSemantics = &H1000000
190             OpenReparsePoint = &H200000
191             OpenNoRecall = &H100000
192             FirstPipeInstance = &H80000
193         End Enum
194
195         <StructLayout(LayoutKind.Sequential)>
196         Private Structure REPARSE_DATA_BUFFER
197             ''' <summary>
198             ''' Reparse point tag. Must be a Microsoft reparse point tag.
199             ''' </summary>
200             Public ReparseTag As UInteger
201
202             ''' <summary>
203             ''' Size, in bytes, of the data after the Reserved member. This can be calculated by:
204             ''' (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength +
205             ''' (namesAreNullTerminated ? 2 * sizeof(char) : 0);
206             ''' </summary>
207             Public ReparseDataLength As UShort
208
209             ''' <summary>
210             ''' Reserved; do not use.
211             ''' </summary>
212             Public Reserved As UShort
213
214             ''' <summary>
215             ''' Offset, in bytes, of the substitute name string in the PathBuffer array.
216             ''' </summary>
217             Public SubstituteNameOffset As UShort
218
219             ''' <summary>
220             ''' Length, in bytes, of the substitute name string. If this string is null-terminated,
221             ''' SubstituteNameLength does not include space for the null character.
222             ''' </summary>
223             Public SubstituteNameLength As UShort
224
225             ''' <summary>
226             ''' Offset, in bytes, of the print name string in the PathBuffer array.
227             ''' </summary>
228             Public PrintNameOffset As UShort
229
230             ''' <summary>
231             ''' Length, in bytes, of the print name string. If this string is null-terminated,
232             ''' PrintNameLength does not include space for the null character.
233             ''' </summary>
234             Public PrintNameLength As UShort
235
236             ''' <summary>
237             ''' A buffer containing the unicode-encoded path string. The path string contains
238             ''' the substitute name string and print name string.
239             ''' </summary>
240             <MarshalAs(UnmanagedType.ByValArray, SizeConst:=&H3FF0)>
241             Public PathBuffer As Byte()
242         End Structure
243
244         <DllImport("kernel32.dll"CharSet:=CharSet.Auto, SetLastError:=True)>
245         Private Function DeviceIoControl(hDevice As IntPtr,
246                                          dwIoControlCode As UInteger,
247                                          InBuffer As IntPtr,
248                                          nInBufferSize As Integer,
249                                          OutBuffer As IntPtr,
250                                          nOutBufferSize As Integer,
251                                          ByRef pBytesReturned As Integer,
252                                          lpOverlapped As IntPtr) As Boolean
253         End Function
254
255         <DllImport("kernel32.dll"SetLastError:=True)>
256         Private Function CreateFile(lpFileName As String,
257                                     dwDesiredAccess As EFileAccess,
258                                     dwShareMode As EFileShare,
259                                     lpSecurityAttributes As IntPtr,
260                                     dwCreationDisposition As ECreationDisposition,
261                                     dwFlagsAndAttributes As EFileAttributes,
262                                     hTemplateFile As IntPtr) As IntPtr
263         End Function
264
265         ''' <summary>
266         ''' Creates a junction point from the specified directory to the specified target directory.
267         ''' </summary>
268         ''' <remarks>
269         ''' Only works on NTFS.
270         ''' </remarks>
271         ''' <param name="junctionPoint">The junction point path</param>
272         ''' <param name="targetDir">The target directory</param>
273         ''' <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
274         ''' <exception cref="IOException">Thrown when the junction point could not be created or when
275         ''' an existing directory was found and <paramref name="overwrite" /> if false</exception>
276         Public Sub Create(junctionPoint As String, targetDir As String, overwrite As Boolean)
277             targetDir = Path.GetFullPath(targetDir)
278
279             If Not Directory.Exists(targetDir) Then
280                 Throw New IOException("Target path does not exist or is not a directory.")
281             End If
282
283             If Directory.Exists(junctionPoint) Then
284                 If Not overwrite Then
285                     Throw New IOException("Directory already exists and overwrite parameter is false.")
286                 End If
287             Else
288                 Directory.CreateDirectory(junctionPoint)
289             End If
290
291             Using handle As SafeFileHandle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)
292                 Dim targetDirBytes As Byte() = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix & Path.GetFullPath(targetDir))
293
294                 Dim reparseDataBuffer As New REPARSE_DATA_BUFFER()
295
296                 reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT
297                 reparseDataBuffer.ReparseDataLength = CUShort(targetDirBytes.Length + 12)
298                 reparseDataBuffer.SubstituteNameOffset = 0
299                 reparseDataBuffer.SubstituteNameLength = CUShort(targetDirBytes.Length)
300                 reparseDataBuffer.PrintNameOffset = CUShort(targetDirBytes.Length + 2)
301                 reparseDataBuffer.PrintNameLength = 0
302                 reparseDataBuffer.PathBuffer = New Byte(16367) {}
303                 Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length)
304
305                 Dim inBufferSize As Integer = Marshal.SizeOf(reparseDataBuffer)
306                 Dim inBuffer As IntPtr = Marshal.AllocHGlobal(inBufferSize)
307
308                 Try
309                     Marshal.StructureToPtr(reparseDataBuffer, inBuffer, False)
310
311                     Dim bytesReturned As Integer
312                     Dim result As Boolean = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT, inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0,
313                         bytesReturned, IntPtr.Zero)
314
315                     If Not result Then
316                         ThrowLastWin32Error("Unable to create junction point.")
317                     End If
318                 Finally
319                     Marshal.FreeHGlobal(inBuffer)
320                 End Try
321             End Using
322         End Sub
323
324         ''' <summary>
325         ''' Deletes a junction point at the specified source directory along with the directory itself.
326         ''' Does nothing if the junction point does not exist.
327         ''' </summary>
328         ''' <remarks>
329         ''' Only works on NTFS.
330         ''' </remarks>
331         ''' <param name="junctionPoint">The junction point path</param>
332         Public Sub Delete(junctionPoint As String)
333             If Not Directory.Exists(junctionPoint) Then
334                 If File.Exists(junctionPoint) Then
335                     Throw New IOException("Path is not a junction point.")
336                 End If
337
338                 Return
339             End If
340
341             Using handle As SafeFileHandle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)
342                 Dim reparseDataBuffer As New REPARSE_DATA_BUFFER()
343
344                 reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT
345                 reparseDataBuffer.ReparseDataLength = 0
346                 reparseDataBuffer.PathBuffer = New Byte(16367) {}
347
348                 Dim inBufferSize As Integer = Marshal.SizeOf(reparseDataBuffer)
349                 Dim inBuffer As IntPtr = Marshal.AllocHGlobal(inBufferSize)
350                 Try
351                     Marshal.StructureToPtr(reparseDataBuffer, inBuffer, False)
352
353                     Dim bytesReturned As Integer
354                     Dim result As Boolean = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT, inBuffer, 8, IntPtr.Zero, 0,
355                         bytesReturned, IntPtr.Zero)
356
357                     If Not result Then
358                         ThrowLastWin32Error("Unable to delete junction point.")
359                     End If
360                 Finally
361                     Marshal.FreeHGlobal(inBuffer)
362                 End Try
363
364                 Try
365                     Directory.Delete(junctionPoint)
366                 Catch ex As IOException
367                     Throw New IOException("Unable to delete junction point.", ex)
368                 End Try
369             End Using
370         End Sub
371
372         ''' <summary>
373         ''' Determines whether the specified path exists and refers to a junction point.
374         ''' </summary>
375         ''' <param name="path">The junction point path</param>
376         ''' <returns>True if the specified path represents a junction point</returns>
377         ''' <exception cref="IOException">Thrown if the specified path is invalid
378         ''' or some other error occurs</exception>
379         Public Function Exists(path As StringAs Boolean
380             If Not Directory.Exists(path) Then
381                 Return False
382             End If
383
384             Using handle As SafeFileHandle = OpenReparsePoint(path, EFileAccess.GenericRead)
385                 Dim target As String = InternalGetTarget(handle)
386                 Return target IsNot Nothing
387             End Using
388         End Function
389
390         ''' <summary>
391         ''' Gets the target of the specified junction point.
392         ''' </summary>
393         ''' <remarks>
394         ''' Only works on NTFS.
395         ''' </remarks>
396         ''' <param name="junctionPoint">The junction point path</param>
397         ''' <returns>The target of the junction point</returns>
398         ''' <exception cref="IOException">Thrown when the specified path does not
399         ''' exist, is invalid, is not a junction point, or some other error occurs</exception>
400         Public Function GetTarget(junctionPoint As StringAs String
401             Using handle As SafeFileHandle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead)
402                 Dim target As String = InternalGetTarget(handle)
403                 If target Is Nothing Then
404                     Throw New IOException("Path is not a junction point.")
405                 End If
406
407                 Return target
408             End Using
409         End Function
410
411         Private Function InternalGetTarget(handle As SafeFileHandle) As String
412             Dim outBufferSize As Integer = Marshal.SizeOf(GetType(REPARSE_DATA_BUFFER))
413             Dim outBuffer As IntPtr = Marshal.AllocHGlobal(outBufferSize)
414
415             Try
416                 Dim bytesReturned As Integer
417                 Dim result As Boolean = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0, outBuffer, outBufferSize,
418                     bytesReturned, IntPtr.Zero)
419
420                 If Not result Then
421                     Dim [error] As Integer = Marshal.GetLastWin32Error()
422                     If [error] = ERROR_NOT_A_REPARSE_POINT Then
423                         Return Nothing
424                     End If
425
426                     ThrowLastWin32Error("Unable to get information about junction point.")
427                 End If
428
429                 Dim reparseDataBuffer As REPARSE_DATA_BUFFER = CType(Marshal.PtrToStructure(outBuffer, GetType(REPARSE_DATA_BUFFER)), REPARSE_DATA_BUFFER)
430
431                 If reparseDataBuffer.ReparseTag <> IO_REPARSE_TAG_MOUNT_POINT Then
432                     Return Nothing
433                 End If
434
435                 Dim targetDir As String = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength)
436
437                 If targetDir.StartsWith(NonInterpretedPathPrefix) Then
438                     targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length)
439                 End If
440
441                 Return targetDir
442             Finally
443                 Marshal.FreeHGlobal(outBuffer)
444             End Try
445         End Function
446
447         Private Function OpenReparsePoint(reparsePoint As String, accessMode As EFileAccess) As SafeFileHandle
448             Dim reparsePointHandle As New SafeFileHandle(CreateFile(reparsePoint, accessMode, EFileShare.Read Or EFileShare.Write Or EFileShare.Delete, IntPtr.Zero, ECreationDisposition.OpenExisting, EFileAttributes.BackupSemantics Or EFileAttributes.OpenReparsePoint,
449                 IntPtr.Zero), True)
450
451             If Marshal.GetLastWin32Error() <> 0 Then
452                 ThrowLastWin32Error("Unable to open reparse point.")
453             End If
454
455             Return reparsePointHandle
456         End Function
457
458         Private Sub ThrowLastWin32Error(message As String)
459             Throw New IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()))
460         End Sub
461     End Module
462 End Namespace