mirror of
https://github.com/Kitware/CMake.git
synced 2026-03-11 20:11:10 -05:00
Merge topic 'path-resolver'
1a6015e5fcPathResolver: Add helper to compute normalized paths2a6f86ec5eTests/CMakeLib: Remove stray output from test loop646f37b473clang-tidy: disable modernize-concat-nested-namespaces Acked-by: Kitware Robot <kwrobot@kitware.com> Acked-by: buildbot <buildbot@kitware.com> Merge-request: !9965
This commit is contained in:
@@ -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,\
|
||||
|
||||
@@ -412,6 +412,8 @@ add_library(
|
||||
cmNewLineStyle.cxx
|
||||
cmOrderDirectories.cxx
|
||||
cmOrderDirectories.h
|
||||
cmPathResolver.cxx
|
||||
cmPathResolver.h
|
||||
cmPlistParser.cxx
|
||||
cmPlistParser.h
|
||||
cmPolicies.h
|
||||
|
||||
541
Source/cmPathResolver.cxx
Normal file
541
Source/cmPathResolver.cxx
Normal file
@@ -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
Source/cmPathResolver.h
Normal file
98
Source/cmPathResolver.h
Normal file
@@ -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>;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ set(CMakeLib_TESTS
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
testOptional.cxx
|
||||
testPathResolver.cxx
|
||||
testString.cxx
|
||||
testStringAlgorithms.cxx
|
||||
testSystemTools.cxx
|
||||
|
||||
@@ -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
Tests/CMakeLib/testPathResolver.cxx
Normal file
476
Tests/CMakeLib/testPathResolver.cxx
Normal file
@@ -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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user