使用 Win32API 获取 Windows 文件符号链接目标位置

2022-07-25 273点热度 0条评论

说起文件符号链接想必很多小伙伴都不陌生,符号链接可以很方便的跨文件系统在多个位置引用同一个文件。但可能很多小伙伴了解最多的是 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();
        }
    }
}

 


如果你有其他相关问题,欢迎加入 QQ 群小山探讨。

微信公众号二维码

微信扫描二维码关注我们

如果觉得文章有帮助到你,可以点击下方的打赏按钮赞助下服务器费用。

小山

一个什么都不会但要装作很厉害的人

文章评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据