std::filesystem::relative和std::filesystem::path::lexically_relative

std::filesystem::relative和std::filesystem::path::lexically_relative

背景

开发的一个在远程主机和本地直接复制粘贴文件的工具,在更新后有些用户反应使用时会崩溃,而多数用户又不崩溃。

分析原因

让崩溃的客户挂上x64dbg,再触发崩溃,得到了崩溃点和调用栈。回去ida看,发现崩溃在这里

对应到源码(几行源码编译成机器码居然膨胀成这个样子,这要是没符号文件那可难分析了)

std::vector<BYTE> ClipboardFileDataObject::ChangeGroupDescriptorToRelative(const std::vector<BYTE>& groupDescriptor)
{
    std::vector<BYTE> ret = groupDescriptor;

    FILEGROUPDESCRIPTORW* fgd = (FILEGROUPDESCRIPTORW*)&ret[0];

    std::filesystem::path firstFile = fgd->fgd[0].cFileName;
    std::filesystem::path root = firstFile.parent_path();

    FILEDESCRIPTORW* fd = fgd->fgd;
    for (int i = 0; i < fgd->cItems; i++)
    {
        std::filesystem::path currentPath = fd[i].cFileName;
        std::filesystem::path relativePath = std::filesystem::relative(currentPath, root);
        wcscpy_s(fd[i].cFileName, relativePath.wstring().c_str());
    }

    return ret;
}

直观上来看,是std::filesystem::relative()抛了异常。可是这凭啥抛异常,我只是想简单计算一个文件路径到另一个路径的相对地址而已。

测试

写了一个简单的测试程序,在我和崩溃客户的机器上,跑的结果不一样。

int main()
{
    std::cout << "Hello World!\n";

    const char* filePath = "D:\\Minmus\\Clipboard\\password123.zip";

    try
    {
        std::filesystem::path path = filePath;
        std::filesystem::path root = path.root_path();
        std::filesystem::path relPath = std::filesystem::relative(path, root);
        std::cout << relPath.string() << std::endl;

    } 
    catch (std::exception e)
    {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

在我机器上不抛异常,客户的机器上抛异常。然后去搜了下filesystem的文档,发现这个std::filesystem::relative()函数居然要求传入的路径是已存在的,妈耶。也就是说,往relative()函数传不存在的路径,后果是未定义的,可以成功,可以不成功。

又进一步测试,发现大多数情况,即使路径不存在,这个函数也能正常工作,但是如果路径的盘符恰好和系统中光驱的盘符一致,它就会抛出异常:weakly_canonical: 设备未就绪。而我那个用户是个虚拟机用户,d盘就是光驱的位置,所以这个函数boom了。

结论

std::filesystem::relative()必须保证传入的路径在本机已存在,否则会出现异常行为。因为它需要处理符号连接等内容。官方文档:

Returns p made relative to base. Resolves symlinks and normalizes both p and base before other processing. Effectively returns weakly_canonical(p).lexically_relative(weakly_canonical(base)) or weakly_canonical(p, ec).lexically_relative(weakly_canonical(base, ec)), except the error code form returns path() at the first error occurrence, if any

Parameters
p – an existing path
base – base path, against which p will be made relative/proximate
ec – error code to store error status to

其实文档里面也说了,如果只是需要获得语义上的相对路径,使用lexically_relative()即可。

These conversions are purely lexical. They do not check that the paths exist, do not follow symlinks, and do not access the filesystem at all. For symlink-following counterparts of lexically_relative and lexically_proximate, see relative and proximate.

看来还是cpp官方考虑的周全。

修复

使用std::filesystem::path::lexically_relative()即可。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注