说起文件符号链接想必很多小伙伴都不陌生,符号链接可以很方便的跨文件系统在多个位置引用同一个文件。但可能很多小伙伴了解最多的是 Linux 的符号链接,Windows 的符号链接了解的比较少。的确,Windows 的符号链接存在感太低了,因为它默认只能通过命令创建,而且需要管理员权限,这就导致它没 Linux 的符号链接那么实用。
小山最近就在写一个跟符号链接相关的工具,因为是用 AutoHotKey 写的,所以关于符号链接的所有操作只能通过调用 Win32API 实现,一切都很顺利,直到我想获取符号链接指向的目标位置,问题就来了。
我通过搜索找到了微软开发博客的这篇文章《How do I get information about the target of a symbolic link?》,起初我顺利通过GetFinalPathNameByHandle
函数获取到了符号链接的目标位置,但是当我获取指向由 ImDisk 创建的内存盘内的文件的符号链接时,发现获取到的是空字符串,然后我就纳闷这是为啥呢?
GetFinalPathNameByHandle
函数最后一个dwFlags
参数支持指定获取到的路径格式,默认是VOLUME_NAME_DOS
,也就是 DOS 风格的卷名模式,接着我尝试了其他卷名模式,发现获取指向内存盘的符号链接时,只有 NT 和 不带卷名风格可以获取到路径。具体原因我也不是很清楚,不过看GetFinalPathNameByHandle
函数的备注,可能是 Windows 并不认为内存盘是已安装的驱动器。找到问题后,我就开始想解决方法。
因为只有 NT 和不带卷名的风格可以获取到路径,所以我只能从这两种风格返回的字符串解决问题,首先我想到的是从 NT 风格解决,因为它返回的是 Windows NT 设备路径,路径里有每个驱动器的设备路径,比较方便定位驱动器属于那个盘符。但是我并没有找到有什么简便方法可以获取 NT 路径和对应盘符的关系,有个QueryDosDevice
函数可以通过卷标获取到对应的 NT 路径,那我想着列举出系统所有的卷标,然后依次获取对比,网上也有类似的解决方案。但是,这个函数需要管理员权限才能正常工作,虽然我们上面说过了 Windows 创建符号链接需要管理员权限,但是读取是不需要的啊,由于我是把这些方法都包装成了会用到的工具方法,是当作”库“来写的,我不想让这个方法被迫需要管理员权限,所以解析 NT 路径就被否定了。
只剩下不带卷名风格的路径可以使用了,既然没有卷名,那就得通过其他方法获取到目标所在的卷。然后我发现 Win32API 有个GetVolumeInformationByHandle
函数可以通过文件句柄获取到文件所在卷的信息,虽然它获取到的信息并不包含卷标,但是包含一个卷序列号,这个序列号是系统格式化分区的时候为其分配的,所以对于每个分区来说是唯一的,所以我只要列举出系统的所有卷,再依次获取每个卷的序列号,与其匹配,就可以给不带卷名的路径加上卷名,这样就是一个完整的路径了。
在写完这篇文章之后,我又尝试了QueryDosDevice
函数,发现它并不需要管理员权限就可以工作,至于之前是为什么,可能是因为我用 AutoHotKey 调用的时候传参有问题。但是这个函数依然有一些问题,对于网络映射卷,QueryDosDevice
获取到的路径和GetFinalPathNameByHandle
有区别,所以最后我想到了另一种方法,就是通过GetFinalPathNameByHandle
依次获取每个卷的 NT 路径,然后与符号链接的路径前缀相比较,这样就可以精准找到符号链接的卷标了。
我已经更新了示例代码和 AutoHotKey 辅助库。
由于 AutoHotKey 有列举所有卷的方法,对于没有对应方法的编程语言可以使用GetLogicalDriveStrings
Win32API 列出所有卷。
问题到此就结束了,如果你有更完美的方法欢迎告诉我。由于小山并不会 C 和 C++,下面附上一份 C# (.NET Framework 4) 的代码,以及有 AutoHotKey 需求的小伙伴可以在 https://gist.github.com/Hill-98/cec377910e9b20d182d1002a0abb84e1 获取符号链接操作辅助库。
using System; using System.IO; using System.Linq; using System.Text; using System.Runtime.InteropServices; namespace SymbolicLink { class Program { [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr CreateFile( [MarshalAs(UnmanagedType.LPTStr)] string lpFileName, [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode, IntPtr lpSecurityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr templateFile); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.I1)] static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, uint dwFlags); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool CloseHandle(IntPtr hObject); [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern uint GetFinalPathNameByHandle(IntPtr hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr GetLogicalDriveStrings(uint nBufferLength, [Out] char[] lpBuffer); static void Main() { /** * 以下代码只是演示如何获取符号链接的目标位置,并没有对代码可能失败的情况做任何处理,也没有处理一些错误。 */ // 创建一个符号链接 var linkPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); File.WriteAllBytes(tempPath, new byte[] { 0x00 }); CreateSymbolicLink(linkPath, tempPath, 0x2); // 0x02 允许开启开发者模式后进程未提权时创建符号链接 Console.WriteLine("{0} -> {1}", linkPath, tempPath); // 获取一个符号链接的目标位置 var targetPathBuilder = new StringBuilder(260); var handle = CreateFile(linkPath, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0x02000000, IntPtr.Zero); // 打开一个文件获得句柄 GetFinalPathNameByHandle(handle, targetPathBuilder, 260, 0x2); // 获取 NT 风格的的符号链接目标路径 CloseHandle(handle); // 关闭打开的文件句柄 var targetPath = targetPathBuilder.ToString(); Console.WriteLine("targetPath: {0}", targetPath); var drives = new char[100]; GetLogicalDriveStrings(100, drives); // 获取系统上所有驱动器 drives = drives.Where((c) => c.CompareTo('A') >= 0 && c.CompareTo('Z') <= 0).ToArray(); Console.WriteLine("AllDrives: {0}", string.Join(",", drives)); foreach (var drive in drives) { var driveRootPath = $"{drive}:\\"; var driveNtPath = new StringBuilder(260); var driveHandle = CreateFile(driveRootPath, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0x02000000, IntPtr.Zero); // 打开一个卷获得句柄 GetFinalPathNameByHandle(driveHandle, driveNtPath, 260, 0x2); // 获取 NT 风格的卷路径 Console.WriteLine("{0}: {1}", drive, driveNtPath); CloseHandle(driveHandle); // 关闭打开的卷句柄 if (targetPath.StartsWith(driveNtPath.ToString())) { targetPath = driveRootPath + targetPath.Substring(driveNtPath.ToString().Length); break; } } Console.WriteLine("{0} -> {1}", linkPath, targetPath); Console.ReadLine(); } } }
微信扫描二维码关注我们
如果觉得文章有帮助到你,可以点击下方的打赏按钮赞助下服务器费用。
文章评论