Browse Source

Merge topic 'path-resolver'

1a6015e5fc PathResolver: Add helper to compute normalized paths
2a6f86ec5e Tests/CMakeLib: Remove stray output from test loop
646f37b473 clang-tidy: disable modernize-concat-nested-namespaces

Acked-by: Kitware Robot <[email protected]>
Acked-by: buildbot <[email protected]>
Merge-request: !9965
Brad King 1 year ago
parent
commit
f9f4ab55ac

+ 1 - 0
.clang-tidy

@@ -29,6 +29,7 @@ misc-*,\
 -misc-use-internal-linkage,\
 modernize-*,\
 -modernize-avoid-c-arrays,\
+-modernize-concat-nested-namespaces,\
 -modernize-macro-to-enum,\
 -modernize-return-braced-init-list,\
 -modernize-type-traits,\

+ 2 - 0
Source/CMakeLists.txt

@@ -412,6 +412,8 @@ add_library(
   cmNewLineStyle.cxx
   cmOrderDirectories.cxx
   cmOrderDirectories.h
+  cmPathResolver.cxx
+  cmPathResolver.h
   cmPlistParser.cxx
   cmPlistParser.h
   cmPolicies.h

+ 541 - 0
Source/cmPathResolver.cxx

@@ -0,0 +1,541 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt 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);
+    }
+  }
+
+#ifdef _WIN32
+  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
+#ifdef _WIN32
+      && !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
+{
+#ifdef _WIN32
+  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 RealPath
+{
+#ifdef _WIN32
+  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
+{
+#ifdef _WIN32
+  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 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::NaivePath>;
+
+}
+}

+ 98 - 0
Source/cmPathResolver.h

@@ -0,0 +1,98 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <string>
+
+#include "cmsys/Status.hxx"
+
+namespace cm {
+namespace PathResolver {
+
+class System;
+
+/** Normalize filesystem paths according to a Policy.
+ *
+ * Resolved paths are always absolute, have no '..', '.', or empty
+ * components, and have a trailing '/' if and only if the entire
+ * path is a root component.
+ *
+ * The Policy determines behavior w.r.t. symbolic links, existence,
+ * and matching the on-disk case (upper/lower) of existing paths.
+ */
+template <class Policy>
+class Resolver
+{
+  System& OS;
+
+public:
+  /** Construct with a concrete filesystem access implementation.  */
+  Resolver(System& os);
+
+  /** Resolve the input path according to the Policy, if possible.
+      On success, the resolved path is stored in 'out'.
+      On failure, the non-existent path is stored in 'out'.  */
+  cmsys::Status Resolve(std::string in, std::string& out) const;
+};
+
+/** Access the filesystem via runtime dispatch.
+    This allows unit tests to work without accessing a real filesystem,
+    which is particularly important on Windows where symbolic links
+    may not be something we can create without administrator privileges.
+    */
+class System
+{
+public:
+  System();
+  virtual ~System() = 0;
+
+  /** If the given path is a symbolic link, read its target.
+      If the path exists but is not a symbolic link, fail
+      with EINVAL or ERROR_NOT_A_REPARSE_POINT.  */
+  virtual cmsys::Status ReadSymlink(std::string const& path,
+                                    std::string& symlink_target) = 0;
+
+  /** Return whether the given path exists on disk.  */
+  virtual bool PathExists(std::string const& path) = 0;
+
+  /** Get the process's working directory.  */
+  virtual std::string GetWorkingDirectory() = 0;
+
+#ifdef _WIN32
+  /** Get the process's working directory on a Windows drive letter.
+      This is a legacy DOS concept supported by 'cmd' shells.  */
+  virtual std::string GetWorkingDirectoryOnDrive(char drive_letter) = 0;
+
+  /** Read the on-disk spelling of the last component of a file path.  */
+  virtual cmsys::Status ReadName(std::string const& path,
+                                 std::string& name) = 0;
+#endif
+};
+
+namespace Policies {
+// IWYU pragma: begin_exports
+
+/** 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).  */
+struct LogicalPath;
+
+/** Normalizes paths while resolving all symlinks.
+    Requires paths to exist, and reads their on-disk case (on Windows).  */
+struct RealPath;
+
+/** Normalizes paths in memory without disk access.
+    Assumes components followed by '..' components are not symlinks.  */
+struct NaivePath;
+
+// IWYU pragma: end_exports
+}
+
+extern template class Resolver<Policies::LogicalPath>;
+extern template class Resolver<Policies::RealPath>;
+extern template class Resolver<Policies::NaivePath>;
+
+}
+}

+ 1 - 0
Tests/CMakeLib/CMakeLists.txt

@@ -23,6 +23,7 @@ set(CMakeLib_TESTS
   testRST.cxx
   testRange.cxx
   testOptional.cxx
+  testPathResolver.cxx
   testString.cxx
   testStringAlgorithms.cxx
   testSystemTools.cxx

+ 2 - 3
Tests/CMakeLib/testCommon.h

@@ -40,10 +40,9 @@ inline int runTests(std::initializer_list<std::function<bool()>> const& tests,
         break;
       }
     }
-    std::cout << '.';
   }
-  if (!result) {
-    std::cout << " Passed\n";
+  if (result == 0) {
+    std::cout << "Passed\n";
   }
   return result;
 }

+ 476 - 0
Tests/CMakeLib/testPathResolver.cxx

@@ -0,0 +1,476 @@
+/* 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
+  });
+}

+ 1 - 0
bootstrap

@@ -459,6 +459,7 @@ CMAKE_CXX_SOURCES="\
   cmOutputConverter \
   cmParseArgumentsCommand \
   cmPathLabel \
+  cmPathResolver \
   cmPolicies \
   cmProcessOutput \
   cmProjectCommand \