/* 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 #include #include #include #include #include #include #include #ifdef _WIN32 # include # include #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 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 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 Control Impl::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 ImplBase::ReadSymlink(std::string const& path, cmsys::Status& status) { cm::optional 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(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 Control Impl::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(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 Control Impl::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 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 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 Control Impl::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 cmsys::Status Impl::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 Resolver::Resolver(System& os) : OS(os) { } template cmsys::Status Resolver::Resolve(std::string in, std::string& out) const { return Impl(OS).Resolve(std::move(in), out); } System::System() = default; System::~System() = default; template class Resolver; template class Resolver; template class Resolver; template class Resolver; } }