/* 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>;

}
}
