From ade2cc81829f7b52b48cc70e9d3105fa3c8c4665 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 11 Dec 2023 01:47:14 +1000 Subject: [PATCH] Path: Add RealPath() --- common/FileSystem.cpp | 157 ++++++++++++++++++++++++++++++ common/Path.h | 3 + tests/ctest/common/path_tests.cpp | 40 +++++--- 3 files changed, 187 insertions(+), 13 deletions(-) diff --git a/common/FileSystem.cpp b/common/FileSystem.cpp index 461f098494..2a5c2a9811 100644 --- a/common/FileSystem.cpp +++ b/common/FileSystem.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #ifdef __APPLE__ #include @@ -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 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(0), + [](size_t l, const std::string_view& s) { return l + s.length(); }) + + components.size() + 1); + +#ifdef _WIN32 + std::wstring wrealpath; + std::vector 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(symlink_buf.size()), + FILE_NAME_NORMALIZED); + if (ret > symlink_buf.size()) + { + symlink_buf.resize(ret); + ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast(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(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(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); } diff --git a/common/Path.h b/common/Path.h index 99f07992ee..4c6c20f1ed 100644 --- a/common/Path.h +++ b/common/Path.h @@ -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); diff --git a/tests/ctest/common/path_tests.cpp b/tests/ctest/common/path_tests.cpp index ed4e0a112f..2da697d441 100644 --- a/tests/ctest/common/path_tests.cpp +++ b/tests/ctest/common/path_tests.cpp @@ -17,7 +17,7 @@ #include "common/Path.h" #include -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")); @@ -238,4 +238,18 @@ TEST(FileSystem, ChangeFileName) #else ASSERT_EQ(Path::ChangeFileName("/foo/bar", "baz"), "/foo/baz"); #endif -} \ No newline at end of file +} + +#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