17
votes

I'm trying to create an NTFS Junction. From the cmd line I can do this using the junction.exe tool from sysinternals. The output of a DIR cmd for a junction looks like this:

 Volume in drive C has no label.
 Volume Serial Number is C8BC-2EBD

 Directory of c:\users\cheeso\Documents

03/22/2009  09:45 PM    <JUNCTION>     My Music [\??\c:\users\cheeso\Music]
05/11/2007  05:42 PM    <DIR>          My Received Files
03/22/2009  09:46 PM    <JUNCTION>     my videos [\??\c:\users\cheeso\Videos]

I read somewhere that Junctions are a subset of Symbolic Links.

So I tried using CreateSymbolicLink to create a Junction. When I do this, I actually get a Symlink, not a junction.

09/09/2009  11:50 AM    <SYMLINKD>     newLink [.\]

There is also CreateHardLink. The doc there says junctions (aka "Reparse Points") are a subset of hardlinks. but I can't seem to get this call to work. It completes but there is no hardlink or junction created.

I'm using .NET/C# and the imports look like this:

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateSymbolicLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern int CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateHardLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern bool CreateHardLink(string lpFileName,
                                             string lpExistingFileName,
                                             IntPtr mustBeNull);

What am I doing wrong?
How can I create a Junction from within C#?

4

4 Answers

15
votes

It looks like you can, and somebody's created a library on CodeProject that has a number of functions they've built in C# to work with Junction points.

http://www.codeproject.com/KB/files/JunctionPointsNet.aspx

It looks like he's actually using the following DllImport to accomplish it:

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
        IntPtr InBuffer, int nInBufferSize,
        IntPtr OutBuffer, int nOutBufferSize,
        out int pBytesReturned, IntPtr lpOverlapped);
2
votes

The code is from http://www.codeproject.com/KB/files/JunctionPointsNet.aspx as a shortcut for those who can not reach this link or a backup in case the original page is down.

Do not vote up this answser, since I'm not author of this code.

Thanks to the original author jeff.brown@codeproject.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace Monitor.Core.Utilities
{
    /// <summary>
    /// Provides access to NTFS junction points in .Net.
    /// </summary>
    public static class JunctionPoint
    {
        /// <summary>
        /// The file or directory is not a reparse point.
        /// </summary>
        private const int ERROR_NOT_A_REPARSE_POINT = 4390;

        /// <summary>
        /// The reparse point attribute cannot be set because it conflicts with an existing attribute.
        /// </summary>
        private const int ERROR_REPARSE_ATTRIBUTE_CONFLICT = 4391;

        /// <summary>
        /// The data present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_INVALID_REPARSE_DATA = 4392;

        /// <summary>
        /// The tag present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_REPARSE_TAG_INVALID = 4393;

        /// <summary>
        /// There is a mismatch between the tag specified in the request and the tag present in the reparse point.
        /// </summary>
        private const int ERROR_REPARSE_TAG_MISMATCH = 4394;

        /// <summary>
        /// Command to set the reparse point data block.
        /// </summary>
        private const int FSCTL_SET_REPARSE_POINT = 0x000900A4;

        /// <summary>
        /// Command to get the reparse point data block.
        /// </summary>
        private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;

        /// <summary>
        /// Command to delete the reparse point data base.
        /// </summary>
        private const int FSCTL_DELETE_REPARSE_POINT = 0x000900AC;

        /// <summary>
        /// Reparse point tag used to identify mount points and junction points.
        /// </summary>
        private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;

        /// <summary>
        /// This prefix indicates to NTFS that the path is to be treated as a non-interpreted
        /// path in the virtual file system.
        /// </summary>
        private const string NonInterpretedPathPrefix = @"\??\";

        [Flags]
        private enum EFileAccess : uint
        {
            GenericRead = 0x80000000,
            GenericWrite = 0x40000000,
            GenericExecute = 0x20000000,
            GenericAll = 0x10000000,
        }

        [Flags]
        private enum EFileShare : uint
        {
            None = 0x00000000,
            Read = 0x00000001,
            Write = 0x00000002,
            Delete = 0x00000004,
        }

        private enum ECreationDisposition : uint
        {
            New = 1,
            CreateAlways = 2,
            OpenExisting = 3,
            OpenAlways = 4,
            TruncateExisting = 5,
        }

        [Flags]
        private enum EFileAttributes : uint
        {
            Readonly = 0x00000001,
            Hidden = 0x00000002,
            System = 0x00000004,
            Directory = 0x00000010,
            Archive = 0x00000020,
            Device = 0x00000040,
            Normal = 0x00000080,
            Temporary = 0x00000100,
            SparseFile = 0x00000200,
            ReparsePoint = 0x00000400,
            Compressed = 0x00000800,
            Offline = 0x00001000,
            NotContentIndexed = 0x00002000,
            Encrypted = 0x00004000,
            Write_Through = 0x80000000,
            Overlapped = 0x40000000,
            NoBuffering = 0x20000000,
            RandomAccess = 0x10000000,
            SequentialScan = 0x08000000,
            DeleteOnClose = 0x04000000,
            BackupSemantics = 0x02000000,
            PosixSemantics = 0x01000000,
            OpenReparsePoint = 0x00200000,
            OpenNoRecall = 0x00100000,
            FirstPipeInstance = 0x00080000
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct REPARSE_DATA_BUFFER
        {
            /// <summary>
            /// Reparse point tag. Must be a Microsoft reparse point tag.
            /// </summary>
            public uint ReparseTag;

            /// <summary>
            /// Size, in bytes, of the data after the Reserved member. This can be calculated by:
            /// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength + 
            /// (namesAreNullTerminated ? 2 * sizeof(char) : 0);
            /// </summary>
            public ushort ReparseDataLength;

            /// <summary>
            /// Reserved; do not use. 
            /// </summary>
            public ushort Reserved;

            /// <summary>
            /// Offset, in bytes, of the substitute name string in the PathBuffer array.
            /// </summary>
            public ushort SubstituteNameOffset;

            /// <summary>
            /// Length, in bytes, of the substitute name string. If this string is null-terminated,
            /// SubstituteNameLength does not include space for the null character.
            /// </summary>
            public ushort SubstituteNameLength;

            /// <summary>
            /// Offset, in bytes, of the print name string in the PathBuffer array.
            /// </summary>
            public ushort PrintNameOffset;

            /// <summary>
            /// Length, in bytes, of the print name string. If this string is null-terminated,
            /// 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.
            /// </summary>
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)]
            public byte[] PathBuffer;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
            IntPtr InBuffer, int nInBufferSize,
            IntPtr OutBuffer, int nOutBufferSize,
            out int pBytesReturned, IntPtr lpOverlapped);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFile(
            string lpFileName,
            EFileAccess dwDesiredAccess,
            EFileShare dwShareMode,
            IntPtr lpSecurityAttributes,
            ECreationDisposition dwCreationDisposition,
            EFileAttributes dwFlagsAndAttributes,
            IntPtr hTemplateFile);

        /// <summary>
        /// Creates a junction point from the specified directory to the specified target directory.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <param name="targetDir">The target directory</param>
        /// <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
        /// <exception cref="IOException">Thrown when the junction point could not be created or when
        /// an existing directory was found and <paramref name="overwrite" /> if false</exception>
        public static void Create(string junctionPoint, string targetDir, bool overwrite)
        {
            targetDir = Path.GetFullPath(targetDir);

            if (!Directory.Exists(targetDir))
                throw new IOException("Target path does not exist or is not a directory.");

            if (Directory.Exists(junctionPoint))
            {
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");
            }
            else
            {
                Directory.CreateDirectory(junctionPoint);
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                byte[] targetDirBytes = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix + Path.GetFullPath(targetDir));

                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = (ushort)(targetDirBytes.Length + 12);
                reparseDataBuffer.SubstituteNameOffset = 0;
                reparseDataBuffer.SubstituteNameLength = (ushort)targetDirBytes.Length;
                reparseDataBuffer.PrintNameOffset = (ushort)(targetDirBytes.Length + 2);
                reparseDataBuffer.PrintNameLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];
                Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length);

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);

                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT,
                        inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to create junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }
            }
        }

        /// <summary>
        /// Deletes a junction point at the specified source directory along with the directory itself.
        /// Does nothing if the junction point does not exist.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        public static void Delete(string junctionPoint)
        {
            if (!Directory.Exists(junctionPoint))
            {
                if (File.Exists(junctionPoint))
                    throw new IOException("Path is not a junction point.");

                return;
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);
                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT,
                        inBuffer, 8, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to delete junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }

                try
                {
                    Directory.Delete(junctionPoint);
                }
                catch (IOException ex)
                {
                    throw new IOException("Unable to delete junction point.", ex);
                }
            }
        }

        /// <summary>
        /// Determines whether the specified path exists and refers to a junction point.
        /// </summary>
        /// <param name="path">The junction point path</param>
        /// <returns>True if the specified path represents a junction point</returns>
        /// <exception cref="IOException">Thrown if the specified path is invalid
        /// or some other error occurs</exception>
        public static bool Exists(string path)
        {
            if (! Directory.Exists(path))
                return false;

            using (SafeFileHandle handle = OpenReparsePoint(path, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                return target != null;
            }
        }

        /// <summary>
        /// Gets the target of the specified junction point.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <returns>The target of the junction point</returns>
        /// <exception cref="IOException">Thrown when the specified path does not
        /// exist, is invalid, is not a junction point, or some other error occurs</exception>
        public static string GetTarget(string junctionPoint)
        {
            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                if (target == null)
                    throw new IOException("Path is not a junction point.");

                return target;
            }
        }

        private static string InternalGetTarget(SafeFileHandle handle)
        {
            int outBufferSize = Marshal.SizeOf(typeof(REPARSE_DATA_BUFFER));
            IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize);

            try
            {
                int bytesReturned;
                bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT,
                    IntPtr.Zero, 0, outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);

                if (!result)
                {
                    int error = Marshal.GetLastWin32Error();
                    if (error == ERROR_NOT_A_REPARSE_POINT)
                        return null;

                    ThrowLastWin32Error("Unable to get information about junction point.");
                }

                REPARSE_DATA_BUFFER reparseDataBuffer = (REPARSE_DATA_BUFFER)
                    Marshal.PtrToStructure(outBuffer, typeof(REPARSE_DATA_BUFFER));

                if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
                    return null;

                string targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
                    reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);

                if (targetDir.StartsWith(NonInterpretedPathPrefix))
                    targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length);

                return targetDir;
            }
            finally
            {
                Marshal.FreeHGlobal(outBuffer);
            }
        }

        private static SafeFileHandle OpenReparsePoint(string reparsePoint, EFileAccess accessMode)
        {
            SafeFileHandle reparsePointHandle = new SafeFileHandle(CreateFile(reparsePoint, accessMode,
                EFileShare.Read | EFileShare.Write | EFileShare.Delete,
                IntPtr.Zero, ECreationDisposition.OpenExisting,
                EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint, IntPtr.Zero), true);

            if (Marshal.GetLastWin32Error() != 0)
                ThrowLastWin32Error("Unable to open reparse point.");

            return reparsePointHandle;
        }

        private static void ThrowLastWin32Error(string message)
        {
            throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
        }
    }
}
1
votes

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
0
votes

For whoever uses this article https://www.codeproject.com/Articles/15633/Manipulating-NTFS-Junction-Points-in-NET
As mentioned in @Walkman and @SqlRyan answers, I added a fix to Jeff Brown's article for when trying to create a junction point to itself.

for example:
dir structure is:
appFolder
current (junction point points to appFolder)
When trying to create a junction point 'current' that points to 'current' (with the overwrite flag), the junction point gets messed up

Here's the fix:

if (Directory.Exists(junctionPoint))
{
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");

                var junctionPointFullPath = Path.GetFullPath(junctionPoint);
                var targetDirFullPath = Path.GetFullPath(targetDir);
                if (junctionPointFullPath.Equals(targetDirFullPath))
                    throw new RecursiveJunctionPointException("Junction point path and target dir can't be identical");
}