mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-23 06:15:38 +00:00
Path: Add RealPath()
This commit is contained in:
parent
b166ec3403
commit
524625269f
|
@ -5,7 +5,7 @@
|
||||||
#include "common/types.h"
|
#include "common/types.h"
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
TEST(FileSystem, ToNativePath)
|
TEST(Path, ToNativePath)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::ToNativePath(""), "");
|
ASSERT_EQ(Path::ToNativePath(""), "");
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ TEST(FileSystem, ToNativePath)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, IsAbsolute)
|
TEST(Path, IsAbsolute)
|
||||||
{
|
{
|
||||||
ASSERT_FALSE(Path::IsAbsolute(""));
|
ASSERT_FALSE(Path::IsAbsolute(""));
|
||||||
ASSERT_FALSE(Path::IsAbsolute("foo"));
|
ASSERT_FALSE(Path::IsAbsolute("foo"));
|
||||||
|
@ -61,7 +61,7 @@ TEST(FileSystem, IsAbsolute)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, Canonicalize)
|
TEST(Path, Canonicalize)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::Canonicalize(""), Path::ToNativePath(""));
|
ASSERT_EQ(Path::Canonicalize(""), Path::ToNativePath(""));
|
||||||
ASSERT_EQ(Path::Canonicalize("foo/bar/../baz"), Path::ToNativePath("foo/baz"));
|
ASSERT_EQ(Path::Canonicalize("foo/bar/../baz"), Path::ToNativePath("foo/baz"));
|
||||||
|
@ -72,9 +72,7 @@ TEST(FileSystem, Canonicalize)
|
||||||
ASSERT_EQ(Path::Canonicalize("./foo"), Path::ToNativePath("foo"));
|
ASSERT_EQ(Path::Canonicalize("./foo"), Path::ToNativePath("foo"));
|
||||||
ASSERT_EQ(Path::Canonicalize("../foo"), Path::ToNativePath("../foo"));
|
ASSERT_EQ(Path::Canonicalize("../foo"), Path::ToNativePath("../foo"));
|
||||||
ASSERT_EQ(Path::Canonicalize("foo/b🙃ar/../b🙃az/./foo"), Path::ToNativePath("foo/b🙃az/foo"));
|
ASSERT_EQ(Path::Canonicalize("foo/b🙃ar/../b🙃az/./foo"), Path::ToNativePath("foo/b🙃az/foo"));
|
||||||
ASSERT_EQ(
|
ASSERT_EQ(Path::Canonicalize("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az/../foℹ︎o"),
|
||||||
Path::Canonicalize(
|
|
||||||
"ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az/../foℹ︎o"),
|
|
||||||
Path::ToNativePath("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/foℹ︎o"));
|
Path::ToNativePath("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/foℹ︎o"));
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
ASSERT_EQ(Path::Canonicalize("C:\\foo\\bar\\..\\baz\\.\\foo"), "C:\\foo\\baz\\foo");
|
ASSERT_EQ(Path::Canonicalize("C:\\foo\\bar\\..\\baz\\.\\foo"), "C:\\foo\\baz\\foo");
|
||||||
|
@ -87,7 +85,7 @@ TEST(FileSystem, Canonicalize)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, Combine)
|
TEST(Path, Combine)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::Combine("", ""), Path::ToNativePath(""));
|
ASSERT_EQ(Path::Combine("", ""), Path::ToNativePath(""));
|
||||||
ASSERT_EQ(Path::Combine("foo", "bar"), Path::ToNativePath("foo/bar"));
|
ASSERT_EQ(Path::Combine("foo", "bar"), Path::ToNativePath("foo/bar"));
|
||||||
|
@ -108,7 +106,7 @@ TEST(FileSystem, Combine)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, AppendDirectory)
|
TEST(Path, AppendDirectory)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::AppendDirectory("foo/bar", "baz"), Path::ToNativePath("foo/baz/bar"));
|
ASSERT_EQ(Path::AppendDirectory("foo/bar", "baz"), Path::ToNativePath("foo/baz/bar"));
|
||||||
ASSERT_EQ(Path::AppendDirectory("", "baz"), Path::ToNativePath("baz"));
|
ASSERT_EQ(Path::AppendDirectory("", "baz"), Path::ToNativePath("baz"));
|
||||||
|
@ -122,7 +120,7 @@ TEST(FileSystem, AppendDirectory)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, MakeRelative)
|
TEST(Path, MakeRelative)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::MakeRelative("", ""), Path::ToNativePath(""));
|
ASSERT_EQ(Path::MakeRelative("", ""), Path::ToNativePath(""));
|
||||||
ASSERT_EQ(Path::MakeRelative("foo", ""), Path::ToNativePath("foo"));
|
ASSERT_EQ(Path::MakeRelative("foo", ""), Path::ToNativePath("foo"));
|
||||||
|
@ -141,8 +139,7 @@ TEST(FileSystem, MakeRelative)
|
||||||
ASSERT_EQ(Path::MakeRelative(A "foo/b🙃ar", A "foo/b🙃az"), Path::ToNativePath("../b🙃ar"));
|
ASSERT_EQ(Path::MakeRelative(A "foo/b🙃ar", A "foo/b🙃az"), Path::ToNativePath("../b🙃ar"));
|
||||||
ASSERT_EQ(Path::MakeRelative(A "f🙃oo/b🙃ar", A "f🙃oo/b🙃az"), Path::ToNativePath("../b🙃ar"));
|
ASSERT_EQ(Path::MakeRelative(A "f🙃oo/b🙃ar", A "f🙃oo/b🙃az"), Path::ToNativePath("../b🙃ar"));
|
||||||
ASSERT_EQ(
|
ASSERT_EQ(
|
||||||
Path::MakeRelative(A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃ar",
|
Path::MakeRelative(A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃ar", A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az"),
|
||||||
A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az"),
|
|
||||||
Path::ToNativePath("../b🙃ar"));
|
Path::ToNativePath("../b🙃ar"));
|
||||||
|
|
||||||
#undef A
|
#undef A
|
||||||
|
@ -154,7 +151,7 @@ TEST(FileSystem, MakeRelative)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, GetExtension)
|
TEST(Path, GetExtension)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::GetExtension("foo"), "");
|
ASSERT_EQ(Path::GetExtension("foo"), "");
|
||||||
ASSERT_EQ(Path::GetExtension("foo.txt"), "txt");
|
ASSERT_EQ(Path::GetExtension("foo.txt"), "txt");
|
||||||
|
@ -164,7 +161,7 @@ TEST(FileSystem, GetExtension)
|
||||||
ASSERT_EQ(Path::GetExtension("a/b/foo"), "");
|
ASSERT_EQ(Path::GetExtension("a/b/foo"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, GetFileName)
|
TEST(Path, GetFileName)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::GetFileName(""), "");
|
ASSERT_EQ(Path::GetFileName(""), "");
|
||||||
ASSERT_EQ(Path::GetFileName("foo"), "foo");
|
ASSERT_EQ(Path::GetFileName("foo"), "foo");
|
||||||
|
@ -179,7 +176,7 @@ TEST(FileSystem, GetFileName)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, GetFileTitle)
|
TEST(Path, GetFileTitle)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::GetFileTitle(""), "");
|
ASSERT_EQ(Path::GetFileTitle(""), "");
|
||||||
ASSERT_EQ(Path::GetFileTitle("foo"), "foo");
|
ASSERT_EQ(Path::GetFileTitle("foo"), "foo");
|
||||||
|
@ -193,7 +190,7 @@ TEST(FileSystem, GetFileTitle)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, GetDirectory)
|
TEST(Path, GetDirectory)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::GetDirectory(""), "");
|
ASSERT_EQ(Path::GetDirectory(""), "");
|
||||||
ASSERT_EQ(Path::GetDirectory("foo"), "");
|
ASSERT_EQ(Path::GetDirectory("foo"), "");
|
||||||
|
@ -207,7 +204,7 @@ TEST(FileSystem, GetDirectory)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, ChangeFileName)
|
TEST(Path, ChangeFileName)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::ChangeFileName("", ""), Path::ToNativePath(""));
|
ASSERT_EQ(Path::ChangeFileName("", ""), Path::ToNativePath(""));
|
||||||
ASSERT_EQ(Path::ChangeFileName("", "bar"), Path::ToNativePath("bar"));
|
ASSERT_EQ(Path::ChangeFileName("", "bar"), Path::ToNativePath("bar"));
|
||||||
|
@ -227,13 +224,14 @@ TEST(FileSystem, ChangeFileName)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FileSystem, SanitizeFileName)
|
TEST(Path, SanitizeFileName)
|
||||||
{
|
{
|
||||||
ASSERT_EQ(Path::SanitizeFileName("foo"), "foo");
|
ASSERT_EQ(Path::SanitizeFileName("foo"), "foo");
|
||||||
ASSERT_EQ(Path::SanitizeFileName("foo/bar"), "foo_bar");
|
ASSERT_EQ(Path::SanitizeFileName("foo/bar"), "foo_bar");
|
||||||
ASSERT_EQ(Path::SanitizeFileName("f🙃o"), "f🙃o");
|
ASSERT_EQ(Path::SanitizeFileName("f🙃o"), "f🙃o");
|
||||||
ASSERT_EQ(Path::SanitizeFileName("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱"), "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱");
|
ASSERT_EQ(Path::SanitizeFileName("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱"), "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱");
|
||||||
ASSERT_EQ(Path::SanitizeFileName("abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}"), "abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}");
|
ASSERT_EQ(Path::SanitizeFileName("abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}"),
|
||||||
|
"abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}");
|
||||||
ASSERT_EQ(Path::SanitizeFileName("some*path**with*asterisks"), "some_path__with_asterisks");
|
ASSERT_EQ(Path::SanitizeFileName("some*path**with*asterisks"), "some_path__with_asterisks");
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
ASSERT_EQ(Path::SanitizeFileName("foo:"), "foo_");
|
ASSERT_EQ(Path::SanitizeFileName("foo:"), "foo_");
|
||||||
|
@ -244,3 +242,17 @@ TEST(FileSystem, SanitizeFileName)
|
||||||
#endif
|
#endif
|
||||||
ASSERT_EQ(Path::SanitizeFileName("foo/bar", false), "foo/bar");
|
ASSERT_EQ(Path::SanitizeFileName("foo/bar", false), "foo/bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
|
||||||
|
// Relies on presence of files.
|
||||||
|
TEST(Path, RealPath)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
ASSERT_EQ(Path::RealPath("C:\\Users\\Me\\Desktop\\foo\\baz"), "C:\\Users\\Me\\Desktop\\foo\\bar\\baz");
|
||||||
|
#else
|
||||||
|
ASSERT_EQ(Path::RealPath("/lib/foo/bar"), "/usr/lib/foo/bar");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -12,6 +12,7 @@
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include <mach-o/dyld.h>
|
#include <mach-o/dyld.h>
|
||||||
|
@ -192,6 +193,161 @@ bool Path::IsAbsolute(const std::string_view& path)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Path::RealPath(const std::string_view& path)
|
||||||
|
{
|
||||||
|
// Resolve non-absolute paths first.
|
||||||
|
std::vector<std::string_view> components;
|
||||||
|
if (!IsAbsolute(path))
|
||||||
|
components = Path::SplitNativePath(Path::Combine(FileSystem::GetWorkingDirectory(), path));
|
||||||
|
else
|
||||||
|
components = Path::SplitNativePath(path);
|
||||||
|
|
||||||
|
std::string realpath;
|
||||||
|
if (components.empty())
|
||||||
|
return realpath;
|
||||||
|
|
||||||
|
// Different to path because relative.
|
||||||
|
realpath.reserve(std::accumulate(components.begin(), components.end(), static_cast<size_t>(0),
|
||||||
|
[](size_t l, const std::string_view& s) { return l + s.length(); }) +
|
||||||
|
components.size() + 1);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::wstring wrealpath;
|
||||||
|
std::vector<WCHAR> symlink_buf;
|
||||||
|
wrealpath.reserve(realpath.size());
|
||||||
|
symlink_buf.resize(path.size() + 1);
|
||||||
|
|
||||||
|
// Check for any symbolic links throughout the path while adding components.
|
||||||
|
bool test_symlink = true;
|
||||||
|
for (const std::string_view& comp : components)
|
||||||
|
{
|
||||||
|
if (!realpath.empty())
|
||||||
|
realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
|
||||||
|
realpath.append(comp);
|
||||||
|
if (test_symlink)
|
||||||
|
{
|
||||||
|
DWORD attribs;
|
||||||
|
if (StringUtil::UTF8StringToWideString(wrealpath, realpath) &&
|
||||||
|
(attribs = GetFileAttributesW(wrealpath.c_str())) != INVALID_FILE_ATTRIBUTES)
|
||||||
|
{
|
||||||
|
// if not a link, go to the next component
|
||||||
|
if (attribs & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||||
|
{
|
||||||
|
const HANDLE hFile =
|
||||||
|
CreateFileW(wrealpath.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
||||||
|
if (hFile != INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
// is a link! resolve it.
|
||||||
|
DWORD ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()),
|
||||||
|
FILE_NAME_NORMALIZED);
|
||||||
|
if (ret > symlink_buf.size())
|
||||||
|
{
|
||||||
|
symlink_buf.resize(ret);
|
||||||
|
ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()),
|
||||||
|
FILE_NAME_NORMALIZED);
|
||||||
|
}
|
||||||
|
if (ret != 0)
|
||||||
|
StringUtil::WideStringToUTF8String(realpath, std::wstring_view(symlink_buf.data(), ret));
|
||||||
|
else
|
||||||
|
test_symlink = false;
|
||||||
|
|
||||||
|
CloseHandle(hFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// not a file or link
|
||||||
|
test_symlink = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFinalPathNameByHandleW() adds a \\?\ prefix, so remove it.
|
||||||
|
if (realpath.starts_with("\\\\?\\") && IsAbsolute(std::string_view(realpath.data() + 4, realpath.size() - 4)))
|
||||||
|
realpath.erase(0, 4);
|
||||||
|
|
||||||
|
#else
|
||||||
|
// Why this monstrosity instead of calling realpath()? realpath() only works on files that exist.
|
||||||
|
std::string basepath;
|
||||||
|
std::string symlink;
|
||||||
|
|
||||||
|
basepath.reserve(realpath.capacity());
|
||||||
|
symlink.resize(realpath.capacity());
|
||||||
|
|
||||||
|
// Check for any symbolic links throughout the path while adding components.
|
||||||
|
bool test_symlink = true;
|
||||||
|
for (const std::string_view& comp : components)
|
||||||
|
{
|
||||||
|
if (!test_symlink)
|
||||||
|
{
|
||||||
|
realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
|
||||||
|
realpath.append(comp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
basepath = realpath;
|
||||||
|
if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER)
|
||||||
|
realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
|
||||||
|
realpath.append(comp);
|
||||||
|
|
||||||
|
// Check if the last component added is a symlink
|
||||||
|
struct stat sb;
|
||||||
|
if (lstat(realpath.c_str(), &sb) != 0)
|
||||||
|
{
|
||||||
|
// Don't bother checking any further components once we error out.
|
||||||
|
test_symlink = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (!S_ISLNK(sb.st_mode))
|
||||||
|
{
|
||||||
|
// Nope, keep going.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
ssize_t sz = readlink(realpath.c_str(), symlink.data(), symlink.size());
|
||||||
|
if (sz < 0)
|
||||||
|
{
|
||||||
|
// shouldn't happen, due to the S_ISLNK check above.
|
||||||
|
test_symlink = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (static_cast<size_t>(sz) == symlink.size())
|
||||||
|
{
|
||||||
|
// need a larger buffer
|
||||||
|
symlink.resize(symlink.size() * 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// is a link, and we resolved it. gotta check if the symlink itself is relative :(
|
||||||
|
symlink.resize(static_cast<size_t>(sz));
|
||||||
|
if (!Path::IsAbsolute(symlink))
|
||||||
|
{
|
||||||
|
// symlink is relative to the directory of the symlink
|
||||||
|
realpath = basepath;
|
||||||
|
if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER)
|
||||||
|
realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
|
||||||
|
realpath.append(symlink);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use the new, symlinked path.
|
||||||
|
realpath = symlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return realpath;
|
||||||
|
}
|
||||||
|
|
||||||
std::string Path::ToNativePath(const std::string_view& path)
|
std::string Path::ToNativePath(const std::string_view& path)
|
||||||
{
|
{
|
||||||
std::string ret;
|
std::string ret;
|
||||||
|
@ -1382,6 +1538,7 @@ std::string FileSystem::GetProgramPath()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Windows symlinks don't behave silly like Linux, so no need to RealPath() it.
|
||||||
return StringUtil::WideStringToUTF8String(buffer);
|
return StringUtil::WideStringToUTF8String(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,9 @@ void SanitizeFileName(std::string* str, bool strip_slashes = true);
|
||||||
/// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix).
|
/// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix).
|
||||||
bool IsAbsolute(const std::string_view& path);
|
bool IsAbsolute(const std::string_view& path);
|
||||||
|
|
||||||
|
/// Resolves any symbolic links in the specified path.
|
||||||
|
std::string RealPath(const std::string_view& path);
|
||||||
|
|
||||||
/// Makes the specified path relative to another (e.g. /a/b/c, /a/b -> ../c).
|
/// Makes the specified path relative to another (e.g. /a/b/c, /a/b -> ../c).
|
||||||
/// Both paths must be relative, otherwise this function will just return the input path.
|
/// Both paths must be relative, otherwise this function will just return the input path.
|
||||||
std::string MakeRelative(const std::string_view& path, const std::string_view& relative_to);
|
std::string MakeRelative(const std::string_view& path, const std::string_view& relative_to);
|
||||||
|
|
Loading…
Reference in a new issue