Files
CMake/Tests/CMakeLib/testPathResolver.cxx
Brad King 1a6015e5fc PathResolver: Add helper to compute normalized paths
Create a `cm::PathResolver` helper to compute normalized paths.
Provide a common implementation with compile-time dispatch to
select details w.r.t. symbolic links, existence, and matching
the on-disk case of existing paths.  Later we can use this to
implement:

* `ToNormalizedPathOnDisk`: Normalizes paths while resolving symlinks
  only when followed by `..` components.  Does not require paths to
  exist, but reads on-disk case of paths that do exist (on Windows).

* `GetRealPath`: Normalizes paths while resolving all symlinks.
   Requires paths to exist, and reads their on-disk case (on Windows).

* `CollapseFullPath`: Normalizes paths in memory without disk access.
  Assumes components followed by `..` components are not symlinks.

Abstract filesystem access through runtime dispatch so that we can test
Windows symbolic link and network path behavior without relying on real
environments.  The overhead of runtime dispatch should be insignificant
during real filesystem access.

Issue: #16228
Issue: #17206
2024-11-01 08:44:17 -04:00

477 lines
13 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include <cmConfigure.h> // IWYU pragma: keep
#include <cerrno>
#include <map>
#include <string>
#include <utility>
#include <cmsys/Status.hxx>
#include "cmPathResolver.h"
#ifdef _WIN32
# include <cctype>
# include "cmSystemTools.h"
#endif
#include "testCommon.h"
// IWYU pragma: no_forward_declare cm::PathResolver::Policies::LogicalPath
// IWYU pragma: no_forward_declare cm::PathResolver::Policies::NaivePath
// IWYU pragma: no_forward_declare cm::PathResolver::Policies::RealPath
namespace {
class MockSystem : public cm::PathResolver::System
{
public:
~MockSystem() override = default;
struct Path
{
std::string Name;
std::string Link;
};
std::map<std::string, Path> Paths;
void SetPaths(std::map<std::string, Path> paths)
{
this->Paths = std::move(paths);
}
static std::string AdjustCase(std::string const& path)
{
#ifdef _WIN32
return cmSystemTools::LowerCase(path);
#else
return path;
#endif
}
cmsys::Status ReadSymlink(std::string const& path,
std::string& link) override
{
auto i = this->Paths.find(AdjustCase(path));
if (i == this->Paths.end()) {
return cmsys::Status::POSIX(ENOENT);
}
if (i->second.Link.empty()) {
return cmsys::Status::POSIX(EINVAL);
}
link = i->second.Link;
return cmsys::Status::Success();
}
bool PathExists(std::string const& path) override
{
return this->Paths.find(AdjustCase(path)) != this->Paths.end();
}
std::string WorkDir;
void SetWorkDir(std::string wd) { this->WorkDir = std::move(wd); }
std::string GetWorkingDirectory() override { return this->WorkDir; }
#ifdef _WIN32
std::map<char, std::string> WorkDirOnDrive;
void SetWorkDirOnDrive(std::map<char, std::string> wd)
{
this->WorkDirOnDrive = std::move(wd);
}
std::string GetWorkingDirectoryOnDrive(char letter) override
{
std::string result;
auto i = this->WorkDirOnDrive.find(std::tolower(letter));
if (i != this->WorkDirOnDrive.end()) {
result = i->second;
}
return result;
}
cmsys::Status ReadName(std::string const& path, std::string& name) override
{
auto i = this->Paths.find(AdjustCase(path));
if (i == this->Paths.end()) {
return cmsys::Status::POSIX(ENOENT);
}
name = i->second.Name;
return cmsys::Status::Success();
}
#endif
};
#define EXPECT_RESOLVE(_in, _expect) \
do { \
std::string out; \
ASSERT_TRUE(r.Resolve(_in, out)); \
ASSERT_EQUAL(out, _expect); \
} while (false)
#define EXPECT_ENOENT(_in, _expect) \
do { \
std::string out; \
ASSERT_EQUAL(r.Resolve(_in, out).GetPOSIX(), ENOENT); \
ASSERT_EQUAL(out, _expect); \
} while (false)
using namespace cm::PathResolver;
bool posixRoot()
{
std::cout << "posixRoot()\n";
MockSystem os;
os.SetPaths({
{ "/", { {}, {} } },
});
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("/", "/");
EXPECT_RESOLVE("//", "/");
EXPECT_RESOLVE("/.", "/");
EXPECT_RESOLVE("/./", "/");
EXPECT_RESOLVE("/..", "/");
EXPECT_RESOLVE("/../", "/");
return true;
}
bool posixAbsolutePath()
{
std::cout << "posixAbsolutePath()\n";
MockSystem os;
os.SetPaths({
{ "/", { {}, {} } },
{ "/a", { {}, {} } },
});
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("/a", "/a");
EXPECT_RESOLVE("/a/", "/a");
EXPECT_RESOLVE("/a//", "/a");
EXPECT_RESOLVE("/a/.", "/a");
EXPECT_RESOLVE("/a/./", "/a");
EXPECT_RESOLVE("/a/..", "/");
EXPECT_RESOLVE("/a/../", "/");
EXPECT_RESOLVE("/a/../..", "/");
#ifndef _WIN32
EXPECT_RESOLVE("//a", "/a");
#endif
return true;
}
bool posixWorkingDirectory()
{
std::cout << "posixWorkingDirectory()\n";
MockSystem os;
os.SetPaths({
{ "/", { {}, {} } },
{ "/a", { {}, {} } },
{ "/cwd", { {}, {} } },
{ "/cwd/a", { {}, {} } },
});
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("", "/");
EXPECT_RESOLVE(".", "/");
EXPECT_RESOLVE("..", "/");
EXPECT_RESOLVE("a", "/a");
os.SetWorkDir("/cwd");
EXPECT_RESOLVE("", "/cwd");
EXPECT_RESOLVE(".", "/cwd");
EXPECT_RESOLVE("..", "/");
EXPECT_RESOLVE("a", "/cwd/a");
return true;
}
bool posixSymlink()
{
std::cout << "posixSymlink()\n";
MockSystem os;
os.SetPaths({
{ "/", { {}, {} } },
{ "/link-a", { {}, "a" } },
{ "/link-a-excess", { {}, "a//." } },
{ "/link-broken", { {}, "link-broken-dest" } },
{ "/a", { {}, {} } },
{ "/a/b", { {}, {} } },
{ "/a/link-b", { {}, "b" } },
{ "/a/b/link-c", { {}, "c" } },
{ "/a/b/c", { {}, {} } },
{ "/a/b/c/link-..|..", { {}, "../.." } },
{ "/a/link-|1|2", { {}, "/1/2" } },
{ "/1", { {}, {} } },
{ "/1/2", { {}, {} } },
{ "/1/2/3", { {}, {} } },
});
{
Resolver<Policies::LogicalPath> const r(os);
EXPECT_RESOLVE("/link-a", "/link-a");
EXPECT_RESOLVE("/link-a-excess", "/link-a-excess");
EXPECT_RESOLVE("/link-a-excess/b", "/link-a-excess/b");
EXPECT_RESOLVE("/link-broken", "/link-broken");
EXPECT_RESOLVE("/link-a/../missing", "/missing");
EXPECT_RESOLVE("/a/b/link-c", "/a/b/link-c");
EXPECT_RESOLVE("/a/link-b/c", "/a/link-b/c");
EXPECT_RESOLVE("/a/link-b/link-c/..", "/a/link-b");
EXPECT_RESOLVE("/a/b/c/link-..|..", "/a/b/c/link-..|..");
EXPECT_RESOLVE("/a/b/c/link-..|../link-b", "/a/b/c/link-..|../link-b");
EXPECT_RESOLVE("/a/link-|1|2/3", "/a/link-|1|2/3");
EXPECT_RESOLVE("/a/link-|1|2/../2/3", "/1/2/3");
}
{
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("/link-a", "/a");
EXPECT_RESOLVE("/link-a-excess", "/a");
EXPECT_RESOLVE("/link-a-excess/b", "/a/b");
EXPECT_ENOENT("/link-broken", "/link-broken-dest");
EXPECT_ENOENT("/link-a/../missing", "/missing");
EXPECT_RESOLVE("/a/b/link-c", "/a/b/c");
EXPECT_RESOLVE("/a/link-b/c", "/a/b/c");
EXPECT_RESOLVE("/a/link-b/link-c/..", "/a/b");
EXPECT_RESOLVE("/a/b/c/link-..|..", "/a");
EXPECT_RESOLVE("/a/b/c/link-..|../link-b", "/a/b");
EXPECT_RESOLVE("/a/link-|1|2/3", "/1/2/3");
}
return true;
}
#ifdef _WIN32
bool windowsRoot()
{
std::cout << "windowsRoot()\n";
MockSystem os;
{
Resolver<Policies::NaivePath> const r(os);
EXPECT_RESOLVE("c:/", "c:/");
EXPECT_RESOLVE("C:/", "C:/");
EXPECT_RESOLVE("c://", "c:/");
EXPECT_RESOLVE("C:/.", "C:/");
EXPECT_RESOLVE("c:/./", "c:/");
EXPECT_RESOLVE("C:/..", "C:/");
EXPECT_RESOLVE("c:/../", "c:/");
}
os.SetPaths({
{ "c:/", { {}, {} } },
{ "//host/", { {}, {} } },
});
{
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("c:/", "C:/");
EXPECT_RESOLVE("C:/", "C:/");
EXPECT_RESOLVE("c://", "C:/");
EXPECT_RESOLVE("C:/.", "C:/");
EXPECT_RESOLVE("c:/./", "C:/");
EXPECT_RESOLVE("C:/..", "C:/");
EXPECT_RESOLVE("c:/../", "C:/");
EXPECT_RESOLVE("//host", "//host/");
EXPECT_RESOLVE("//host/.", "//host/");
EXPECT_RESOLVE("//host/./", "//host/");
EXPECT_RESOLVE("//host/..", "//host/");
EXPECT_RESOLVE("//host/../", "//host/");
}
return true;
}
bool windowsAbsolutePath()
{
std::cout << "windowsAbsolutePath()\n";
MockSystem os;
os.SetPaths({
{ "c:/", { {}, {} } },
{ "c:/a", { {}, {} } },
});
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("c:/a", "C:/a");
EXPECT_RESOLVE("c:/a/", "C:/a");
EXPECT_RESOLVE("c:/a//", "C:/a");
EXPECT_RESOLVE("c:/a/.", "C:/a");
EXPECT_RESOLVE("c:/a/./", "C:/a");
EXPECT_RESOLVE("c:/a/..", "C:/");
EXPECT_RESOLVE("c:/a/../", "C:/");
EXPECT_RESOLVE("c:/a/../..", "C:/");
return true;
}
bool windowsActualCase()
{
std::cout << "windowsActualCase()\n";
MockSystem os;
os.SetPaths({
{ "c:/", { {}, {} } },
{ "c:/mixed", { "MiXeD", {} } },
{ "c:/mixed/link-mixed", { "LiNk-MiXeD", "mixed" } },
{ "c:/mixed/mixed", { "MiXeD", {} } },
{ "c:/mixed/link-c-mixed", { "LiNk-C-MiXeD", "C:/mIxEd" } },
{ "c:/upper", { "UPPER", {} } },
{ "c:/upper/link-upper", { "LINK-UPPER", "upper" } },
{ "c:/upper/upper", { "UPPER", {} } },
{ "c:/upper/link-c-upper", { "LINK-C-UPPER", "c:/upper" } },
});
{
Resolver<Policies::LogicalPath> const r(os);
EXPECT_RESOLVE("c:/mIxEd/MiSsInG", "C:/MiXeD/MiSsInG");
EXPECT_RESOLVE("c:/mIxEd/link-MiXeD", "C:/MiXeD/LiNk-MiXeD");
EXPECT_RESOLVE("c:/mIxEd/link-c-MiXeD", "C:/MiXeD/LiNk-C-MiXeD");
EXPECT_RESOLVE("c:/upper/mIsSiNg", "C:/UPPER/mIsSiNg");
EXPECT_RESOLVE("c:/upper/link-upper", "C:/UPPER/LINK-UPPER");
EXPECT_RESOLVE("c:/upper/link-c-upper", "C:/UPPER/LINK-C-UPPER");
}
{
Resolver<Policies::RealPath> const r(os);
EXPECT_ENOENT("c:/mIxEd/MiSsInG", "C:/MiXeD/MiSsInG");
EXPECT_RESOLVE("c:/mIxEd/link-MiXeD", "C:/MiXeD/MiXeD");
EXPECT_RESOLVE("c:/mIxEd/link-c-MiXeD", "C:/MiXeD");
EXPECT_ENOENT("c:/upper/mIsSiNg", "C:/UPPER/mIsSiNg");
EXPECT_RESOLVE("c:/upper/link-upper", "C:/UPPER/UPPER");
EXPECT_RESOLVE("c:/upper/link-c-upper", "C:/UPPER");
}
return true;
}
bool windowsWorkingDirectory()
{
std::cout << "windowsWorkingDirectory()\n";
MockSystem os;
os.SetPaths({
{ "c:/", { {}, {} } },
{ "c:/a", { {}, {} } },
{ "c:/cwd", { {}, {} } },
{ "c:/cwd/a", { {}, {} } },
});
{
Resolver<Policies::LogicalPath> const r(os);
EXPECT_RESOLVE("", "/");
EXPECT_RESOLVE(".", "/");
EXPECT_RESOLVE("..", "/");
EXPECT_RESOLVE("a", "/a");
}
{
Resolver<Policies::RealPath> const r(os);
os.SetWorkDir("c:/cwd");
EXPECT_RESOLVE("", "C:/cwd");
EXPECT_RESOLVE(".", "C:/cwd");
EXPECT_RESOLVE("..", "C:/");
EXPECT_RESOLVE("a", "C:/cwd/a");
EXPECT_ENOENT("missing", "C:/cwd/missing");
}
return true;
}
bool windowsWorkingDirectoryOnDrive()
{
std::cout << "windowsWorkingDirectoryOnDrive()\n";
MockSystem os;
os.SetWorkDir("c:/cwd");
os.SetWorkDirOnDrive({
{ 'd', "d:/cwd-d" },
});
{
Resolver<Policies::NaivePath> const r(os);
EXPECT_RESOLVE("c:", "c:/cwd");
EXPECT_RESOLVE("c:.", "c:/cwd");
EXPECT_RESOLVE("c:..", "c:/");
EXPECT_RESOLVE("C:", "C:/cwd");
EXPECT_RESOLVE("C:.", "C:/cwd");
EXPECT_RESOLVE("C:..", "C:/");
EXPECT_RESOLVE("d:", "d:/cwd-d");
EXPECT_RESOLVE("d:.", "d:/cwd-d");
EXPECT_RESOLVE("d:..", "d:/");
EXPECT_RESOLVE("D:", "D:/cwd-d");
EXPECT_RESOLVE("D:.", "D:/cwd-d");
EXPECT_RESOLVE("D:..", "D:/");
EXPECT_RESOLVE("e:", "e:/");
EXPECT_RESOLVE("e:.", "e:/");
EXPECT_RESOLVE("e:..", "e:/");
EXPECT_RESOLVE("E:", "E:/");
EXPECT_RESOLVE("E:.", "E:/");
EXPECT_RESOLVE("E:..", "E:/");
}
os.SetPaths({
{ "c:/", { {}, {} } },
{ "c:/cwd", { {}, {} } },
{ "c:/cwd/existing", { {}, {} } },
{ "d:/", { {}, {} } },
{ "d:/cwd-d", { {}, {} } },
{ "d:/cwd-d/existing", { {}, {} } },
{ "e:/", { {}, {} } },
});
{
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("c:existing", "C:/cwd/existing");
EXPECT_ENOENT("c:missing", "C:/cwd/missing");
EXPECT_RESOLVE("C:existing", "C:/cwd/existing");
EXPECT_ENOENT("C:missing", "C:/cwd/missing");
EXPECT_RESOLVE("d:existing", "D:/cwd-d/existing");
EXPECT_ENOENT("d:missing", "D:/cwd-d/missing");
EXPECT_ENOENT("e:missing", "E:/missing");
EXPECT_ENOENT("f:", "F:/");
}
return true;
}
bool windowsNetworkShare()
{
std::cout << "windowsNetworkShare()\n";
MockSystem os;
os.SetPaths({
{ "c:/", { {}, {} } },
{ "c:/cwd", { {}, {} } },
{ "c:/cwd/link-to-host-share", { {}, "//host/share" } },
{ "//host/", { {}, {} } },
{ "//host/share", { {}, {} } },
});
os.SetWorkDir("c:/cwd");
{
Resolver<Policies::RealPath> const r(os);
EXPECT_RESOLVE("//host/share", "//host/share");
EXPECT_RESOLVE("//host/share/", "//host/share");
EXPECT_RESOLVE("//host/share/.", "//host/share");
EXPECT_RESOLVE("//host/share/./", "//host/share");
EXPECT_RESOLVE("//host/share/..", "//host/");
EXPECT_RESOLVE("//host/share/../", "//host/");
EXPECT_RESOLVE("//host/share/../..", "//host/");
EXPECT_RESOLVE("link-to-host-share", "//host/share");
EXPECT_RESOLVE("link-to-host-share/..", "//host/");
EXPECT_ENOENT("link-to-host-share/../missing", "//host/missing");
}
{
Resolver<Policies::LogicalPath> const r(os);
EXPECT_RESOLVE("link-to-host-share", "C:/cwd/link-to-host-share");
EXPECT_RESOLVE("link-to-host-share/..", "//host/");
EXPECT_RESOLVE("link-to-host-share/../missing", "//host/missing");
}
return true;
}
#endif
}
int testPathResolver(int /*unused*/, char* /*unused*/[])
{
return runTests({
posixRoot,
posixAbsolutePath,
posixWorkingDirectory,
posixSymlink,
#ifdef _WIN32
windowsRoot,
windowsAbsolutePath,
windowsActualCase,
windowsWorkingDirectory,
windowsWorkingDirectoryOnDrive,
windowsNetworkShare,
#endif
});
}