I have simplified/updated the CreateJunction code by Jeff Brown on CodeProject, e.g. using automatic marshalling to pass the structure along to DeviceIoControl instead of having to manually manage the memory. I've only done this for creating a junction, as you can delete with Directory.Delete() and .Net's GetAttributes returns whether it has a reparse point.
I've also removed the target dir exists check, as I find it useful to be able to create a junction to a folder that doesn't exist or will exist at some later point. (Different drive e.t.c.)
One thing I wasn't able to figure out, was the sizes added to the string length for the structure members and nInBufferSize DeviceIoControl parameter, they don't seem to add up to a Marshal.SizeOf return value.
I did this in VB.Net, so I used IC#Code's CodeConverter extension to convert it to C#:
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public class Junction
{
[Flags]
private enum Win32FileAccess : uint
{
GenericRead = 0x80000000U,
GenericWrite = 0x40000000U,
GenericExecute = 0x20000000U,
GenericAll = 0x10000000U
}
[Flags]
private enum Win32FileAttribute : uint
{
AttributeReadOnly = 0x1U,
AttributeHidden = 0x2U,
AttributeSystem = 0x4U,
AttributeDirectory = 0x10U,
AttributeArchive = 0x20U,
AttributeDevice = 0x40U,
AttributeNormal = 0x80U,
AttributeTemporary = 0x100U,
AttributeSparseFile = 0x200U,
AttributeReparsePoint = 0x400U,
AttributeCompressed = 0x800U,
AttributeOffline = 0x1000U,
AttributeNotContentIndexed = 0x2000U,
AttributeEncrypted = 0x4000U,
AttributeIntegrityStream = 0x8000U,
AttributeVirtual = 0x10000U,
AttributeNoScrubData = 0x20000U,
AttributeEA = 0x40000U,
AttributeRecallOnOpen = 0x40000U,
AttributePinned = 0x80000U,
AttributeUnpinned = 0x100000U,
AttributeRecallOnDataAccess = 0x400000U,
FlagOpenNoRecall = 0x100000U,
/// <summary>
/// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
/// whether or not the filter that controls the reparse point is operational.
/// <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
/// <br />If the file is not a reparse point, then this flag is ignored.
/// </summary>
FlagOpenReparsePoint = 0x200000U,
FlagSessionAware = 0x800000U,
FlagPosixSemantics = 0x1000000U,
/// <summary>
/// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
/// </summary>
FlagBackupSemantics = 0x2000000U,
FlagDeleteOnClose = 0x4000000U,
FlagSequentialScan = 0x8000000U,
FlagRandomAccess = 0x10000000U,
FlagNoBuffering = 0x20000000U,
FlagOverlapped = 0x40000000U,
FlagWriteThrough = 0x80000000U
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern SafeFileHandle CreateFile(string lpFileName, Win32FileAccess dwDesiredAccess,
FileShare dwShareMode, IntPtr lpSecurityAttributes, FileMode dwCreationDisposition,
Win32FileAttribute dwFlagsAndAttributes, IntPtr hTemplateFile);
// Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct ReparseDataBuffer
{
/// <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
public uint ReparseTag;
/// <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
public ushort ReparseDataLength;
/// <summary>Reserved; do not use.</summary>
private ushort Reserved;
/// <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
public ushort SubstituteNameOffset;
/// <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
public ushort SubstituteNameLength;
/// <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
public ushort PrintNameOffset;
/// <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
public ushort PrintNameLength;
/// <summary>
/// A buffer containing the unicode-encoded path string. The path string contains the substitute name
/// string and print name string. The substitute name and print name strings can appear in any order.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8184)]
internal string PathBuffer;
// with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
// 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode,
[In] ReparseDataBuffer lpInBuffer, uint nInBufferSize,
IntPtr lpOutBuffer, uint nOutBufferSize,
[Out] uint lpBytesReturned, IntPtr lpOverlapped);
public static void Create(string junctionPath, string targetDir, bool overwrite = false)
{
const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003U;
const uint FSCTL_SET_REPARSE_POINT = 0x900A4U;
// This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
const string NonInterpretedPathPrefix = @"\??\";
if (Directory.Exists(junctionPath))
{
if (!overwrite)
throw new IOException("Directory already exists and overwrite parameter is false.");
}
else
{
Directory.CreateDirectory(junctionPath);
}
targetDir = NonInterpretedPathPrefix + Path.GetFullPath(targetDir);
using (var reparsePointHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
FileShare.Read | FileShare.Write | FileShare.Delete, IntPtr.Zero, FileMode.Open,
Win32FileAttribute.FlagBackupSemantics | Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero))
{
if (reparsePointHandle.IsInvalid || Marshal.GetLastWin32Error() != 0)
{
throw new IOException("Unable to open reparse point.", new Win32Exception());
}
// unicode string is 2 bytes per character, so *2 to get byte length
ushort byteLength = (ushort)(targetDir.Length * 2);
var reparseDataBuffer = new ReparseDataBuffer()
{
ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
ReparseDataLength = (ushort)(byteLength + 12u),
SubstituteNameOffset = 0,
SubstituteNameLength = byteLength,
PrintNameOffset = (ushort)(byteLength + 2u),
PrintNameLength = 0,
PathBuffer = targetDir
};
bool result = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, (uint)(byteLength + 20), IntPtr.Zero, 0u, 0u, IntPtr.Zero);
if (!result)
throw new IOException("Unable to create junction point.", new Win32Exception());
}
}
}
And the source VB.Net:
Imports System
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
Public Class Junction
<Flags>
Private Enum Win32FileAccess As UInteger
GenericRead = &H80000000UI
GenericWrite = &H40000000
GenericExecute = &H20000000
GenericAll = &H10000000
End Enum
<Flags>
Private Enum Win32FileAttribute As UInteger
AttributeReadOnly = &H1
AttributeHidden = &H2
AttributeSystem = &H4
AttributeDirectory = &H10
AttributeArchive = &H20
AttributeDevice = &H40
AttributeNormal = &H80
AttributeTemporary = &H100
AttributeSparseFile = &H200
AttributeReparsePoint = &H400
AttributeCompressed = &H800
AttributeOffline = &H1000
AttributeNotContentIndexed = &H2000
AttributeEncrypted = &H4000
AttributeIntegrityStream = &H8000
AttributeVirtual = &H10000
AttributeNoScrubData = &H20000
AttributeEA = &H40000
AttributeRecallOnOpen = &H40000
AttributePinned = &H80000
AttributeUnpinned = &H100000
AttributeRecallOnDataAccess = &H400000
FlagOpenNoRecall = &H100000
''' <summary>
''' Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
''' whether or not the filter that controls the reparse point is operational.
''' <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
''' <br />If the file is not a reparse point, then this flag is ignored.
''' </summary>
FlagOpenReparsePoint = &H200000
FlagSessionAware = &H800000
FlagPosixSemantics = &H1000000
''' <summary>
''' You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
''' </summary>
FlagBackupSemantics = &H2000000
FlagDeleteOnClose = &H4000000
FlagSequentialScan = &H8000000
FlagRandomAccess = &H10000000
FlagNoBuffering = &H20000000
FlagOverlapped = &H40000000
FlagWriteThrough = &H80000000UI
End Enum
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Private Shared Function CreateFile(lpFileName As String, dwDesiredAccess As Win32FileAccess,
dwShareMode As FileShare, lpSecurityAttributes As IntPtr,
dwCreationDisposition As FileMode, dwFlagsAndAttributes As Win32FileAttribute,
hTemplateFile As IntPtr) As SafeFileHandle
End Function
' Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Private Structure ReparseDataBuffer
''' <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
Public ReparseTag As UInteger
''' <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
Public ReparseDataLength As UShort
''' <summary>Reserved; do not use.</summary>
Private Reserved As UShort
''' <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
Public SubstituteNameOffset As UShort
''' <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
Public SubstituteNameLength As UShort
''' <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
Public PrintNameOffset As UShort
''' <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
Public PrintNameLength As UShort
''' <summary>
''' A buffer containing the unicode-encoded path string. The path string contains the substitute name
''' string and print name string. The substitute name and print name strings can appear in any order.
''' </summary>
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=8184)>
Friend PathBuffer As String
' with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
' 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
End Structure
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Private Shared Function DeviceIoControl(hDevice As SafeFileHandle, dwIoControlCode As UInteger,
<[In]> ByRef lpInBuffer As ReparseDataBuffer, nInBufferSize As UInteger,
lpOutBuffer As IntPtr, nOutBufferSize As UInteger,
<Out> ByRef lpBytesReturned As UInteger, lpOverlapped As IntPtr) As Boolean
End Function
Public Shared Sub Create(junctionPath As String, targetDir As String, Optional overwrite As Boolean = False)
Const IO_REPARSE_TAG_MOUNT_POINT As UInteger = &HA0000003UI
Const FSCTL_SET_REPARSE_POINT As UInteger = &H900A4
'This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
Const NonInterpretedPathPrefix As String = "\??\"
If Directory.Exists(junctionPath) Then
If Not overwrite Then Throw New IOException("Directory already exists and overwrite parameter is false.")
Else
Directory.CreateDirectory(junctionPath)
End If
targetDir = NonInterpretedPathPrefix & Path.GetFullPath(targetDir)
Using reparsePointHandle As SafeFileHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
FileShare.Read Or FileShare.Write Or FileShare.Delete, IntPtr.Zero, FileMode.Open,
Win32FileAttribute.FlagBackupSemantics Or Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero)
If reparsePointHandle.IsInvalid OrElse Marshal.GetLastWin32Error() <> 0 Then
Throw New IOException("Unable to open reparse point.", New Win32Exception())
End If
' unicode string is 2 bytes per character, so *2 to get byte length
Dim byteLength As UShort = CType(targetDir.Length * 2, UShort)
Dim reparseDataBuffer As New ReparseDataBuffer With {
.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
.ReparseDataLength = byteLength + 12US,
.SubstituteNameOffset = 0,
.SubstituteNameLength = byteLength,
.PrintNameOffset = byteLength + 2US,
.PrintNameLength = 0,
.PathBuffer = targetDir
}
Dim result As Boolean = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, byteLength + 20US, IntPtr.Zero, 0, 0, IntPtr.Zero)
If Not result Then Throw New IOException("Unable to create junction point.", New Win32Exception())
End Using
End Sub
End Class