Path: Add RealPath()

This commit is contained in:
Stenzek 2023-12-11 01:47:14 +10:00 committed by Connor McLaughlin
parent f546ea1f8a
commit ade2cc8182
3 changed files with 187 additions and 13 deletions

View File

@ -25,6 +25,7 @@
#include <cstdlib>
#include <cstring>
#include <limits>
#include <numeric>
#ifdef __APPLE__
#include <mach-o/dyld.h>
@ -223,6 +224,161 @@ bool Path::IsAbsolute(const std::string_view& path)
#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 ret;
@ -1411,6 +1567,7 @@ std::string FileSystem::GetProgramPath()
break;
}
// Windows symlinks don't behave silly like Linux, so no need to RealPath() it.
return StringUtil::WideStringToUTF8String(buffer);
}

View File

@ -47,6 +47,9 @@ namespace Path
/// 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);
/// 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).
/// 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);

View File

@ -17,7 +17,7 @@
#include "common/Path.h"
#include <gtest/gtest.h>
TEST(FileSystem, ToNativePath)
TEST(Path, ToNativePath)
{
ASSERT_EQ(Path::ToNativePath(""), "");
@ -41,7 +41,7 @@ TEST(FileSystem, ToNativePath)
#endif
}
TEST(FileSystem, IsValidFileName)
TEST(Path, IsValidFileName)
{
#if defined(_WIN32) || defined(__APPLE__)
ASSERT_FALSE(Path::IsValidFileName("foo:bar", false));
@ -64,7 +64,7 @@ TEST(FileSystem, IsValidFileName)
ASSERT_FALSE(Path::IsValidFileName("baz/foo", false));
}
TEST(FileSystem, IsAbsolute)
TEST(Path, IsAbsolute)
{
ASSERT_FALSE(Path::IsAbsolute(""));
ASSERT_FALSE(Path::IsAbsolute("foo"));
@ -80,7 +80,7 @@ TEST(FileSystem, IsAbsolute)
#endif
}
TEST(FileSystem, Canonicalize)
TEST(Path, Canonicalize)
{
ASSERT_EQ(Path::Canonicalize(""), Path::ToNativePath(""));
ASSERT_EQ(Path::Canonicalize("foo/bar/../baz"), Path::ToNativePath("foo/baz"));
@ -103,7 +103,7 @@ TEST(FileSystem, Canonicalize)
#endif
}
TEST(FileSystem, Combine)
TEST(Path, Combine)
{
ASSERT_EQ(Path::Combine("", ""), Path::ToNativePath(""));
ASSERT_EQ(Path::Combine("foo", "bar"), Path::ToNativePath("foo/bar"));
@ -124,7 +124,7 @@ TEST(FileSystem, Combine)
#endif
}
TEST(FileSystem, AppendDirectory)
TEST(Path, AppendDirectory)
{
ASSERT_EQ(Path::AppendDirectory("foo/bar", "baz"), Path::ToNativePath("foo/baz/bar"));
ASSERT_EQ(Path::AppendDirectory("", "baz"), Path::ToNativePath("baz"));
@ -138,7 +138,7 @@ TEST(FileSystem, AppendDirectory)
#endif
}
TEST(FileSystem, MakeRelative)
TEST(Path, MakeRelative)
{
ASSERT_EQ(Path::MakeRelative("", ""), Path::ToNativePath(""));
ASSERT_EQ(Path::MakeRelative("foo", ""), Path::ToNativePath("foo"));
@ -167,7 +167,7 @@ TEST(FileSystem, MakeRelative)
#endif
}
TEST(FileSystem, GetExtension)
TEST(Path, GetExtension)
{
ASSERT_EQ(Path::GetExtension("foo"), "");
ASSERT_EQ(Path::GetExtension("foo.txt"), "txt");
@ -177,7 +177,7 @@ TEST(FileSystem, GetExtension)
ASSERT_EQ(Path::GetExtension("a/b/foo"), "");
}
TEST(FileSystem, GetFileName)
TEST(Path, GetFileName)
{
ASSERT_EQ(Path::GetFileName(""), "");
ASSERT_EQ(Path::GetFileName("foo"), "foo");
@ -192,7 +192,7 @@ TEST(FileSystem, GetFileName)
#endif
}
TEST(FileSystem, GetFileTitle)
TEST(Path, GetFileTitle)
{
ASSERT_EQ(Path::GetFileTitle(""), "");
ASSERT_EQ(Path::GetFileTitle("foo"), "foo");
@ -206,7 +206,7 @@ TEST(FileSystem, GetFileTitle)
#endif
}
TEST(FileSystem, GetDirectory)
TEST(Path, GetDirectory)
{
ASSERT_EQ(Path::GetDirectory(""), "");
ASSERT_EQ(Path::GetDirectory("foo"), "");
@ -220,7 +220,7 @@ TEST(FileSystem, GetDirectory)
#endif
}
TEST(FileSystem, ChangeFileName)
TEST(Path, ChangeFileName)
{
ASSERT_EQ(Path::ChangeFileName("", ""), Path::ToNativePath(""));
ASSERT_EQ(Path::ChangeFileName("", "bar"), Path::ToNativePath("bar"));
@ -239,3 +239,17 @@ TEST(FileSystem, ChangeFileName)
ASSERT_EQ(Path::ChangeFileName("/foo/bar", "baz"), "/foo/baz");
#endif
}
#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