diff --git a/src/common-tests/path_tests.cpp b/src/common-tests/path_tests.cpp index c0e585269..03b307e40 100644 --- a/src/common-tests/path_tests.cpp +++ b/src/common-tests/path_tests.cpp @@ -5,7 +5,7 @@ #include "common/types.h" #include -TEST(FileSystem, ToNativePath) +TEST(Path, ToNativePath) { ASSERT_EQ(Path::ToNativePath(""), ""); @@ -29,7 +29,7 @@ TEST(FileSystem, ToNativePath) #endif } -TEST(FileSystem, IsAbsolute) +TEST(Path, IsAbsolute) { ASSERT_FALSE(Path::IsAbsolute("")); ASSERT_FALSE(Path::IsAbsolute("foo")); @@ -61,7 +61,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")); @@ -72,10 +72,8 @@ TEST(FileSystem, Canonicalize) 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( - "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az/../foℹ︎o"), - Path::ToNativePath("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/foℹ︎o")); + ASSERT_EQ(Path::Canonicalize("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az/../foℹ︎o"), + Path::ToNativePath("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/foℹ︎o")); #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 } -TEST(FileSystem, Combine) +TEST(Path, Combine) { ASSERT_EQ(Path::Combine("", ""), Path::ToNativePath("")); ASSERT_EQ(Path::Combine("foo", "bar"), Path::ToNativePath("foo/bar")); @@ -108,7 +106,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")); @@ -122,7 +120,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")); @@ -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 "f🙃oo/b🙃ar", A "f🙃oo/b🙃az"), Path::ToNativePath("../b🙃ar")); ASSERT_EQ( - Path::MakeRelative(A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃ar", - A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az"), + Path::MakeRelative(A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃ar", A "ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱/b🙃az"), Path::ToNativePath("../b🙃ar")); #undef A @@ -154,7 +151,7 @@ TEST(FileSystem, MakeRelative) #endif } -TEST(FileSystem, GetExtension) +TEST(Path, GetExtension) { ASSERT_EQ(Path::GetExtension("foo"), ""); ASSERT_EQ(Path::GetExtension("foo.txt"), "txt"); @@ -164,7 +161,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"); @@ -179,7 +176,7 @@ TEST(FileSystem, GetFileName) #endif } -TEST(FileSystem, GetFileTitle) +TEST(Path, GetFileTitle) { ASSERT_EQ(Path::GetFileTitle(""), ""); ASSERT_EQ(Path::GetFileTitle("foo"), "foo"); @@ -193,7 +190,7 @@ TEST(FileSystem, GetFileTitle) #endif } -TEST(FileSystem, GetDirectory) +TEST(Path, GetDirectory) { ASSERT_EQ(Path::GetDirectory(""), ""); ASSERT_EQ(Path::GetDirectory("foo"), ""); @@ -207,7 +204,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")); @@ -227,13 +224,14 @@ TEST(FileSystem, ChangeFileName) #endif } -TEST(FileSystem, SanitizeFileName) +TEST(Path, SanitizeFileName) { ASSERT_EQ(Path::SanitizeFileName("foo"), "foo"); ASSERT_EQ(Path::SanitizeFileName("foo/bar"), "foo_bar"); ASSERT_EQ(Path::SanitizeFileName("f🙃o"), "f🙃o"); 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"); #ifdef _WIN32 ASSERT_EQ(Path::SanitizeFileName("foo:"), "foo_"); @@ -243,4 +241,18 @@ TEST(FileSystem, SanitizeFileName) ASSERT_EQ(Path::SanitizeFileName("foo\\bar", false), "foo\\bar"); #endif ASSERT_EQ(Path::SanitizeFileName("foo/bar", false), "foo/bar"); -} \ 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 \ No newline at end of file diff --git a/src/common/file_system.cpp b/src/common/file_system.cpp index 4a5fc6b0b..4ce4b9ed1 100644 --- a/src/common/file_system.cpp +++ b/src/common/file_system.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef __APPLE__ #include @@ -192,6 +193,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; @@ -1382,6 +1538,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/src/common/path.h b/src/common/path.h index 7c03c1ad1..12d864719 100644 --- a/src/common/path.h +++ b/src/common/path.h @@ -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). 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);