Symbolic Links in C#

Written by Troy on March 23, 2012 Categories: Uncategorized Tags: , , ,

A helper class for manipulating symbolic links, available in Windows Server 2008, Vista, etc. You may prefer to create a proper class (new SymLink(link, target) for example) – in this project, I have a wrapper like this that abstracts from symlinks and junction points depending on the underlying platform.

First of all, the test fixture:

using System.IO;
using NUnit.Framework;

namespace Something.FileSystem
{
	[TestFixture]
	public class SymbolicLinkFixture
	{
		string testFilePath;

		string testFolderPath;

		string workingDirectory;

		[TestFixtureSetUp]
		public void FixtureSetUp()
		{
			AssertMore.IgnoreIf2003();
		}

		[SetUp]
		public void SetUp()
		{
			workingDirectory = IOHelper.CreateMyTempPath(this);

			testFolderPath = Path.Combine(workingDirectory, "foo");
			Directory.CreateDirectory(testFolderPath);

			testFilePath = Path.Combine(testFolderPath, "bar.txt");
			IOHelper.CreateTestFile(testFilePath);
		}

		[TearDown]
		public void TearDown()
		{
			IOHelper.DeleteMyTempPath(this);
		}

		private void assertDirLink(string linkPath, string targetPath)
		{
			SymbolicLink.CreateDirectoryLink(linkPath, targetPath);
			Assert.IsTrue(Directory.Exists(linkPath),
				"Link was not created?  Dir does not exist: " + linkPath);
			string expectedLinkedFilePath = Path.Combine(linkPath, "bar.txt");
			AssertMore.FilesAreEqual(testFilePath, expectedLinkedFilePath);

			Assert.IsTrue(SymbolicLink.Exists(linkPath),
				"the link was not detected as a symlink");
			Assert.IsFalse(JunctionPoint.Exists(linkPath),
				"the link was detected as a junction point");

			string actual = SymbolicLink.GetTarget(linkPath);
			Assert.AreEqual(targetPath, actual,
				"The retrieved target does not match what I created");
		}

		private void assertFileLink(string linkPath, string target)
		{
			SymbolicLink.CreateFileLink(linkPath, target);
			AssertMore.FileContentsAreEqual(testFilePath, linkPath);

			Assert.IsTrue(SymbolicLink.Exists(linkPath),
				"the link was not detected as a symlink");
		}

		[Test]
		public void CanCreateSymLinkToAbsoluteDirectoryPath()
		{
			string linkPath = Path.Combine(workingDirectory, "link-to-foo");
			assertDirLink(linkPath, testFolderPath);
		}

		[Test]
		public void CanCreateSymLinkToRelativeDirectoryPath()
		{
			string linkPath = Path.Combine(workingDirectory, "link-to-foo");
			assertDirLink(linkPath, "foo");
		}

		[Test]
		public void CanCreateSymlinkToParentDirectoryPath()
		{
			DirectoryInfo subDirectory = Directory.CreateDirectory(
				Path.Combine(workingDirectory, @"buried\under\folders"));
			string linkPath = Path.Combine(subDirectory.FullName, "link-to-foo");
			assertDirLink(linkPath, @"..\..\..\foo");
		}

		[Test]
		[ExpectedException(typeof(DirectoryNotFoundException))]
		public void CanCreateSymLinkToMissingTargetPath()
		{
			string linkPath = Path.Combine(workingDirectory, "link-to-nowhere");
			SymbolicLink.CreateDirectoryLink(linkPath, @"..\nothing\here");
			Assert.IsTrue(Directory.Exists(linkPath),
				"Symlink not created?  Path: " + linkPath);
			Directory.GetFiles(linkPath);
		}

		[Test]
		public void CanCreateLinkToAbsoluteFilePath()
		{
			string linkPath = Path.Combine(testFolderPath, "foolink.txt");
			assertFileLink(linkPath, testFilePath);
		}

		[Test]
		public void CanCreateLinkToRelativeFilePath()
		{
			string linkPath = Path.Combine(testFolderPath, "fooLink.txt");
			assertFileLink(linkPath, "bar.txt");
		}

		[Test]
		public void CanCreateLinkToParentRelativeFilePath()
		{
			DirectoryInfo newDir = Directory.CreateDirectory(
				Path.Combine(workingDirectory, @"buried\under\folders"));
			string linkPath = Path.Combine(newDir.FullName, "fooLink.txt");
			assertFileLink(linkPath, @"..\..\..\foo\bar.txt");
		}

		[Test]
		[ExpectedException(typeof(DirectoryNotFoundException))]
		public void CanCreateLinkToMissingFilePath()
		{
			string linkPath = Path.Combine(workingDirectory, "link-to-nowhere.txt");
			SymbolicLink.CreateFileLink(linkPath, @"..\nothing\here.txt");
			Assert.IsTrue(File.Exists(linkPath),
				"Did not create the symlink?  Path: " + linkPath);
			IOHelper.GetFileData(linkPath);
		}

		[Test]
		public void SymLinkTestsFalseForRealDirectory()
		{
			Assert.IsFalse(SymbolicLink.Exists(testFolderPath));
		}

		[Test]
		public void SymLinkTestsFalseForRealFile()
		{
			Assert.IsFalse(SymbolicLink.Exists(testFilePath));
		}

		[Test]
		public void SymLinkTestFalseForJunctionPoint()
		{
			string linkPath = Path.Combine(workingDirectory, "link-to-foo");
			JunctionPoint.Create(linkPath, testFolderPath, true);
			Assert.IsTrue(Directory.Exists(linkPath), "link not created?");
			Assert.IsTrue(JunctionPoint.Exists(linkPath), "junction does not exists?");
			Assert.IsFalse(SymbolicLink.Exists(linkPath), "incorrectly tested as a symbolic link");
		}

		[Test]
		public void SymLinkTestsFalseIfNoSuchPath()
		{
			Assert.IsFalse(SymbolicLink.Exists(@"c:\foo\bar\baz"));
		}

		[Test]
		[ExpectedException(typeof(DirectoryNotFoundException))]
		public void ExceptionIfAttemptToGetTargetForInvalidPath()
		{
			SymbolicLink.GetTarget(@"c:\foo\bar\baz");
		}

		[Test]
		[ExpectedException(typeof(IOException))]
		public void ExceptionOnAttemptingToReplaceAnExistingDirectory()
		{
			string linkPath = Path.Combine(testFolderPath, "bar");
			Directory.CreateDirectory(linkPath);
			SymbolicLink.CreateDirectoryLink(linkPath, "bleh");
		}
	}
}

Structure allowing us to retrieve reparse point data:

using System.Runtime.InteropServices;

namespace Something.FileSystem
{
	/// 
	/// Refer to http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012%28v=vs.85%29.aspx
	/// 
	[StructLayout(LayoutKind.Sequential)]
	public struct SymbolicLinkReparseData
	{
		// Not certain about this!
		private const int maxUnicodePathLength = 260 * 2;

		public uint ReparseTag;
		public ushort ReparseDataLength;
		public ushort Reserved;
		public ushort SubstituteNameOffset;
		public ushort SubstituteNameLength;
		public ushort PrintNameOffset;
		public ushort PrintNameLength;
		public uint Flags;
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = maxUnicodePathLength)]
		public byte[] PathBuffer;
	}
}

General symbolic link helper functions:

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

namespace Something.FileSystem
{
	public static class SymbolicLink
	{
		private const uint genericReadAccess = 0x80000000;

		private const uint fileFlagsForOpenReparsePointAndBackupSemantics = 0x02200000;

		private const int ioctlCommandGetReparsePoint = 0x000900A8;

		private const uint openExisting = 0x3;

		private const uint pathNotAReparsePointError = 0x80071126;

		private const uint shareModeAll = 0x7; // Read, Write, Delete

		private const uint symLinkTag = 0xA000000C;
        
		private const int targetIsAFile = 0;

        private const int targetIsADirectory = 1;

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

		[DllImport("kernel32.dll", SetLastError = true)]
		static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);

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

		public static void CreateDirectoryLink(string linkPath, string targetPath)
		{
			if (!CreateSymbolicLink(linkPath, targetPath, targetIsADirectory) || Marshal.GetLastWin32Error() != 0)
			{
				try
				{
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
				}
				catch (COMException exception)
				{
					throw new IOException(exception.Message, exception);
				}
			}
		}

        public static void CreateFileLink(string linkPath, string targetPath)
        {
            if (!CreateSymbolicLink(linkPath, targetPath, targetIsAFile))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

		public static bool Exists(string path)
		{
			if (!Directory.Exists(path) && !File.Exists(path))
			{
				return false;
			}
			string target = GetTarget(path);
			return target != null;
		}

		private static SafeFileHandle getFileHandle(string path)
		{
			return CreateFile(path, genericReadAccess, shareModeAll, IntPtr.Zero, openExisting,
				fileFlagsForOpenReparsePointAndBackupSemantics, IntPtr.Zero);
		}

		public static string GetTarget(string path)
		{
			SymbolicLinkReparseData reparseDataBuffer;

			using (SafeFileHandle fileHandle = getFileHandle(path))
			{
				if (fileHandle.IsInvalid)
				{
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
				}

				int outBufferSize = Marshal.SizeOf(typeof(SymbolicLinkReparseData));
				IntPtr outBuffer = IntPtr.Zero;
				try
				{
					outBuffer = Marshal.AllocHGlobal(outBufferSize);
					int bytesReturned;
					bool success = DeviceIoControl(
						fileHandle.DangerousGetHandle(), ioctlCommandGetReparsePoint, IntPtr.Zero, 0,
						outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);

					fileHandle.Close();

					if (!success)
					{
						if (((uint)Marshal.GetHRForLastWin32Error()) == pathNotAReparsePointError)
						{
							return null;
						}
						Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
					}

					reparseDataBuffer = (SymbolicLinkReparseData)Marshal.PtrToStructure(
						outBuffer, typeof(SymbolicLinkReparseData));
				}
				finally
				{
					Marshal.FreeHGlobal(outBuffer);
				}
			}
			if (reparseDataBuffer.ReparseTag != symLinkTag)
			{
				return null;
			}

			string target = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
				reparseDataBuffer.PrintNameOffset, reparseDataBuffer.PrintNameLength);

			return target;
		}
	}
}

Added 2012-Apr-9

If you noticed the reference to the JunctionPoint class in the tests, this in fact refers to Jeff Brown’s nice post here on The Code Project. In the project this was for, I had a wrapper SymLink class that works transparently for Windows 2003 or 2008, utilizing either junction points or symbolic links. In the interests of symmetry, the SymbolicLink class follows his layout.

4 Comments

4 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>