CMake/Source/cmPathResolver.cxx
Brad King 8dfc725cdb PathResolver: Add mode to collapse paths naively and look up on-disk case
In CMake 3.31 and below, `CollapseFullPath` did this on Windows.  KWSys
has since stopped looking up the on-disk case in `CollapseFullPath` to
avoid disk access when most callers only need an in-memory operation.
We currently call `GetActualCaseForPath` explicitly when needed.

Add a mode to `cm::PathResolver` to combine these operations and cache
disk access behind the `System::ReadName` callback.  We will use this to
restore the way CMake 3.31 and below normalized input paths on Windows.

Issue: #26750
Issue: #20214
2025-03-07 18:22:46 -05:00

553 lines
15 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmPathResolver.h"
#include <algorithm>
#include <cerrno>
#include <cstddef>
#include <string>
#include <utility>
#include <cm/optional>
#include <cm/string_view>
#include <cmext/string_view>
#ifdef _WIN32
# include <cctype>
# include <windows.h>
#endif
#define MAX_SYMBOLIC_LINKS 32
namespace cm {
namespace PathResolver {
namespace {
namespace Options {
enum class ActualCase
{
No,
Yes,
};
enum class Symlinks
{
None,
Lazy,
Eager,
};
enum class Existence
{
Agnostic,
Required,
};
}
enum class Root
{
None,
POSIX,
#ifdef _WIN32
Drive,
Network,
#endif
};
struct Control
{
enum class Tag
{
Continue,
Restart,
Error,
};
Tag tag;
union
{
std::string::size_type slash; // data for Continue
cmsys::Status error; // data for Error
};
static Control Continue(std::string::size_type s)
{
Control c{ Tag::Continue };
c.slash = s;
return c;
}
static Control Restart() { return Control{ Tag::Restart }; }
static Control Error(cmsys::Status e)
{
Control c{ Tag::Error };
c.error = e;
return c;
}
private:
Control(Tag t)
: tag(t)
{
}
};
Root ClassifyRoot(cm::string_view p)
{
#ifdef _WIN32
if (p.size() >= 2 && std::isalpha(p[0]) && p[1] == ':') {
return Root::Drive;
}
if (p.size() >= 3 && p[0] == '/' && p[1] == '/' && p[2] != '/') {
return Root::Network;
}
#endif
if (!p.empty() && p[0] == '/') {
return Root::POSIX;
}
return Root::None;
}
class ImplBase
{
protected:
ImplBase(System& os)
: OS(os)
{
}
System& OS;
std::string P;
std::size_t SymlinkDepth = 0;
#ifdef _WIN32
std::string GetWorkingDirectoryOnDrive(char letter);
Control ResolveRootRelative();
#endif
cm::optional<std::string> ReadSymlink(std::string const& path,
cmsys::Status& status);
Control ResolveSymlink(Root root, std::string::size_type slash,
std::string::size_type next_slash,
std::string symlink_target);
};
template <class Policy>
class Impl : public ImplBase
{
Control ResolveRelativePath();
Control ResolveRoot(Root root);
Control ResolveComponent(Root root, std::string::size_type root_slash,
std::string::size_type slash);
Control ResolvePath();
public:
Impl(System& os)
: ImplBase(os)
{
}
cmsys::Status Resolve(std::string in, std::string& out);
};
template <class Policy>
Control Impl<Policy>::ResolveRelativePath()
{
// This is a relative path. Convert it to absolute and restart.
std::string p = this->OS.GetWorkingDirectory();
std::replace(p.begin(), p.end(), '\\', '/');
if (ClassifyRoot(p) == Root::None) {
p.insert(0, 1, '/');
}
if (p.back() != '/') {
p.push_back('/');
}
P.insert(0, p);
return Control::Restart();
}
#ifdef _WIN32
std::string ImplBase::GetWorkingDirectoryOnDrive(char letter)
{
// Use the drive's working directory, if any.
std::string d = this->OS.GetWorkingDirectoryOnDrive(letter);
std::replace(d.begin(), d.end(), '\\', '/');
if (d.size() >= 3 && std::toupper(d[0]) == std::toupper(letter) &&
d[1] == ':' && d[2] == '/') {
d[0] = letter;
d.push_back('/');
return d;
}
// Use the current working directory if the drive matches.
d = this->OS.GetWorkingDirectory();
if (d.size() >= 3 && std::toupper(d[0]) == std::toupper(letter) &&
d[1] == ':' && d[2] == '/') {
d[0] = letter;
d.push_back('/');
return d;
}
// Fall back to the root directory on the drive.
d = "_:/";
d[0] = letter;
return d;
}
Control ImplBase::ResolveRootRelative()
{
// This is a root-relative path. Resolve the root drive and restart.
P.replace(0, 2, this->GetWorkingDirectoryOnDrive(P[0]));
return Control::Restart();
}
#endif
cm::optional<std::string> ImplBase::ReadSymlink(std::string const& path,
cmsys::Status& status)
{
cm::optional<std::string> result;
std::string target;
status = this->OS.ReadSymlink(path, target);
if (status && ++this->SymlinkDepth >= MAX_SYMBOLIC_LINKS) {
status = cmsys::Status::POSIX(ELOOP);
}
if (status) {
if (!target.empty()) {
result = std::move(target);
}
} else if (status.GetPOSIX() == EINVAL
#ifdef _WIN32
|| status.GetWindows() == ERROR_NOT_A_REPARSE_POINT
#endif
) {
// The path was not a symlink.
status = cmsys::Status::Success();
}
return result;
}
Control ImplBase::ResolveSymlink(Root root, std::string::size_type slash,
std::string::size_type next_slash,
std::string symlink_target)
{
std::replace(symlink_target.begin(), symlink_target.end(), '\\', '/');
Root const symlink_target_root = ClassifyRoot(symlink_target);
if (symlink_target_root == Root::None) {
// This is a symlink to a relative path.
// Resolve the symlink, while preserving the leading and
// trailing (if any) slash:
// "*/link/" => "*/dest/"
// ^slash ^slash
P.replace(slash + 1, next_slash - slash - 1, symlink_target);
return Control::Continue(slash);
}
#ifdef _WIN32
if (root == Root::Drive && symlink_target_root == Root::POSIX) {
// This is a symlink to a POSIX absolute path,
// but the current path is on a drive letter. Resolve the
// symlink while preserving the drive letter, and start over:
// "C:/*/link/" => "C:/dest/"
// ^slash (restart)
P.replace(2, next_slash - 2, symlink_target);
return Control::Restart();
}
#else
static_cast<void>(root);
#endif
// This is a symlink to an absolute path.
// Resolve it and start over:
// "*/link/" => "/dest/"
// ^slash (restart)
P.replace(0, next_slash, symlink_target);
return Control::Restart();
}
template <class Policy>
Control Impl<Policy>::ResolveRoot(Root root)
{
if (root == Root::None) {
return this->ResolveRelativePath();
}
// POSIX absolute paths always start with a '/'.
std::string::size_type root_slash = 0;
#ifdef _WIN32
if (root == Root::Drive) {
if (P.size() == 2 || P[2] != '/') {
return this->ResolveRootRelative();
}
if (Policy::ActualCase == Options::ActualCase::Yes) {
// Normalize the drive letter to upper-case.
P[0] = static_cast<char>(std::toupper(P[0]));
}
// The root is a drive letter. The root '/' immediately follows.
root_slash = 2;
} else if (root == Root::Network) {
// The root is a network name. Find the root '/' after it.
root_slash = P.find('/', 2);
if (root_slash == std::string::npos) {
root_slash = P.size();
P.push_back('/');
}
}
#endif
if (Policy::Existence == Options::Existence::Required
#ifdef _WIN32
&& root != Root::Network
#endif
) {
std::string path = P.substr(0, root_slash + 1);
if (!this->OS.PathExists(path)) {
P = std::move(path);
return Control::Error(cmsys::Status::POSIX(ENOENT));
}
}
return Control::Continue(root_slash);
}
template <class Policy>
Control Impl<Policy>::ResolveComponent(Root root,
std::string::size_type root_slash,
std::string::size_type slash)
{
// Look for the '/' or end-of-input that ends this component.
// The sample paths in comments below show the trailing slash
// even if it is actually beyond the end of the path.
std::string::size_type next_slash = P.find('/', slash + 1);
if (next_slash == std::string::npos) {
next_slash = P.size();
}
cm::string_view c =
cm::string_view(P).substr(slash + 1, next_slash - (slash + 1));
if (slash == root_slash) {
if (c.empty() || c == "."_s || c == ".."_s) {
// This is an empty, '.', or '..' component at the root.
// Drop the component and its trailing slash, if any,
// while preserving the root slash:
// "//" => "/"
// "/./" => "/"
// "/../" => "/"
// ^slash ^slash
P.erase(slash + 1, next_slash - slash);
return Control::Continue(slash);
}
} else {
if (c.empty() || c == "."_s) {
// This is an empty or '.' component not at the root.
// Drop the component and its leading slash:
// "*//" => "*/"
// "*/./" => "*/"
// ^slash ^slash
P.erase(slash, next_slash - slash);
return Control::Continue(slash);
}
if (c == ".."_s) {
// This is a '..' component not at the root.
// Rewind to the previous component:
// "*/prev/../" => "*/prev/../"
// ^slash ^slash
next_slash = slash;
slash = P.rfind('/', slash - 1);
if (Policy::Symlinks == Options::Symlinks::Lazy) {
cmsys::Status status;
std::string path = P.substr(0, next_slash);
if (cm::optional<std::string> maybe_symlink_target =
this->ReadSymlink(path, status)) {
return this->ResolveSymlink(root, slash, next_slash,
std::move(*maybe_symlink_target));
}
if (!status && Policy::Existence == Options::Existence::Required) {
P = std::move(path);
return Control::Error(status);
}
}
// This is not a symlink.
// Drop the component, the following '..', and its trailing slash,
// if any, while preserving the (possibly root) leading slash:
// "*/dir/../" => "*/"
// ^slash ^slash
P.erase(slash + 1, next_slash + 3 - slash);
return Control::Continue(slash);
}
}
// This is a named component.
if (Policy::Symlinks == Options::Symlinks::Eager) {
cmsys::Status status;
std::string path = P.substr(0, next_slash);
if (cm::optional<std::string> maybe_symlink_target =
this->ReadSymlink(path, status)) {
return this->ResolveSymlink(root, slash, next_slash,
std::move(*maybe_symlink_target));
}
if (!status && Policy::Existence == Options::Existence::Required) {
P = std::move(path);
return Control::Error(status);
}
}
#if defined(_WIN32) || defined(__APPLE__)
bool exists = false;
if (Policy::ActualCase == Options::ActualCase::Yes) {
std::string name;
std::string path = P.substr(0, next_slash);
if (cmsys::Status status = this->OS.ReadName(path, name)) {
exists = true;
if (!name.empty()) {
// Rename this component:
// "*/name/" => "*/Name/"
// ^slash ^slash
P.replace(slash + 1, next_slash - slash - 1, name);
next_slash = slash + 1 + name.length();
}
} else if (Policy::Existence == Options::Existence::Required) {
P = std::move(path);
return Control::Error(status);
}
}
#endif
if (Policy::Existence == Options::Existence::Required
#if defined(_WIN32) || defined(__APPLE__)
&& !exists
#endif
) {
std::string path = P.substr(0, next_slash);
if (!this->OS.PathExists(path)) {
P = std::move(path);
return Control::Error(cmsys::Status::POSIX(ENOENT));
}
}
// Keep this component:
// "*/name/" => "*/name/"
// ^slash ^slash
return Control::Continue(next_slash);
}
template <class Policy>
Control Impl<Policy>::ResolvePath()
{
Root const root = ClassifyRoot(P);
// Resolve the root component. It always ends in a slash.
Control control = this->ResolveRoot(root);
if (control.tag != Control::Tag::Continue) {
return control;
}
std::string::size_type const root_slash = control.slash;
// Resolve later components. Every iteration that finishes
// the loop body makes progress either by removing a component
// or advancing the slash past it.
for (std::string::size_type slash = root_slash;
P.size() > root_slash + 1 && slash < P.size();) {
control = this->ResolveComponent(root, root_slash, slash);
if (control.tag != Control::Tag::Continue) {
return control;
}
slash = control.slash;
}
return Control::Continue(P.size());
}
template <class Policy>
cmsys::Status Impl<Policy>::Resolve(std::string in, std::string& out)
{
P = std::move(in);
std::replace(P.begin(), P.end(), '\\', '/');
for (;;) {
Control control = this->ResolvePath();
switch (control.tag) {
case Control::Tag::Continue:
out = std::move(P);
return cmsys::Status::Success();
case Control::Tag::Restart:
continue;
case Control::Tag::Error:
out = std::move(P);
return control.error;
};
}
}
}
namespace Policies {
struct NaivePath
{
#if defined(_WIN32) || defined(__APPLE__)
static constexpr Options::ActualCase ActualCase = Options::ActualCase::No;
#endif
static constexpr Options::Symlinks Symlinks = Options::Symlinks::None;
static constexpr Options::Existence Existence = Options::Existence::Agnostic;
};
struct CasePath
{
#if defined(_WIN32) || defined(__APPLE__)
static constexpr Options::ActualCase ActualCase = Options::ActualCase::Yes;
#endif
static constexpr Options::Symlinks Symlinks = Options::Symlinks::None;
static constexpr Options::Existence Existence = Options::Existence::Agnostic;
};
struct RealPath
{
#if defined(_WIN32) || defined(__APPLE__)
static constexpr Options::ActualCase ActualCase = Options::ActualCase::Yes;
#endif
static constexpr Options::Symlinks Symlinks = Options::Symlinks::Eager;
static constexpr Options::Existence Existence = Options::Existence::Required;
};
struct LogicalPath
{
#if defined(_WIN32) || defined(__APPLE__)
static constexpr Options::ActualCase ActualCase = Options::ActualCase::Yes;
#endif
static constexpr Options::Symlinks Symlinks = Options::Symlinks::Lazy;
static constexpr Options::Existence Existence = Options::Existence::Agnostic;
};
#if defined(__SUNPRO_CC)
constexpr Options::Symlinks NaivePath::Symlinks;
constexpr Options::Existence NaivePath::Existence;
constexpr Options::Symlinks CasePath::Symlinks;
constexpr Options::Existence CasePath::Existence;
constexpr Options::Symlinks RealPath::Symlinks;
constexpr Options::Existence RealPath::Existence;
constexpr Options::Symlinks LogicalPath::Symlinks;
constexpr Options::Existence LogicalPath::Existence;
#endif
}
template <class Policy>
Resolver<Policy>::Resolver(System& os)
: OS(os)
{
}
template <class Policy>
cmsys::Status Resolver<Policy>::Resolve(std::string in, std::string& out) const
{
return Impl<Policy>(OS).Resolve(std::move(in), out);
}
System::System() = default;
System::~System() = default;
template class Resolver<Policies::LogicalPath>;
template class Resolver<Policies::RealPath>;
template class Resolver<Policies::CasePath>;
template class Resolver<Policies::NaivePath>;
}
}