From 1d5ffc4d8d07dcf159def259c5ef9f524aef7bd4 Mon Sep 17 00:00:00 2001 From: gtolontop Date: Sat, 2 May 2026 19:12:11 +0200 Subject: [PATCH 1/3] fix(vfs): resolve symlinked mounted devices --- code/components/vfs-core/include/VFSManager.h | 12 +- code/components/vfs-core/src/VFSManager.cpp | 10 + .../vfs-impl-server/include/Manager.h | 14 +- .../vfs-impl-server/src/Manager.cpp | 291 +++++++++++++++--- 4 files changed, 284 insertions(+), 43 deletions(-) diff --git a/code/components/vfs-core/include/VFSManager.h b/code/components/vfs-core/include/VFSManager.h index 06ccd031d9..664ce643a4 100644 --- a/code/components/vfs-core/include/VFSManager.h +++ b/code/components/vfs-core/include/VFSManager.h @@ -39,7 +39,10 @@ class VFS_CORE_EXPORT Manager : public fwRefCountable virtual void Unmount(const std::string& path) = 0; - virtual fwRefContainer GetNativeDevice(void* nativeDevice); + virtual fwRefContainer GetNativeDevice(void* nativeDevice); + + // Used when multiple mounted devices resolve to the same native path. + virtual fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string& preferredMountPrefix); }; VFS_CORE_EXPORT fwRefContainer OpenRead(const std::string& path); @@ -54,8 +57,11 @@ VFS_CORE_EXPORT fwRefContainer Create(const std::string& path, bool crea VFS_CORE_EXPORT fwRefContainer GetDevice(const std::string& path); -VFS_CORE_EXPORT fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath); - +VFS_CORE_EXPORT fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath); + +// Used when multiple mounted devices resolve to the same native path. +VFS_CORE_EXPORT fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string& preferredMountPrefix); + VFS_CORE_EXPORT fwRefContainer GetNativeDevice(void* nativeDevice); VFS_CORE_EXPORT void Mount(fwRefContainer device, const std::string& path); diff --git a/code/components/vfs-core/src/VFSManager.cpp b/code/components/vfs-core/src/VFSManager.cpp index 2ba0307633..13a8e783e4 100644 --- a/code/components/vfs-core/src/VFSManager.cpp +++ b/code/components/vfs-core/src/VFSManager.cpp @@ -90,6 +90,11 @@ fwRefContainer Manager::GetNativeDevice(void* nativeDevice) return nullptr; } +fwRefContainer Manager::FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string&) +{ + return FindDevice(absolutePath, transformedPath); +} + fwRefContainer OpenRead(const std::string& path) { return Instance::Get()->OpenRead(path); @@ -125,6 +130,11 @@ fwRefContainer FindDevice(const std::string& absolutePath, std::string& return Instance::Get()->FindDevice(absolutePath, transformedPath); } +fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string& preferredMountPrefix) +{ + return Instance::Get()->FindDevice(absolutePath, transformedPath, preferredMountPrefix); +} + fwRefContainer GetNativeDevice(void* nativeDevice) { return Instance::Get()->GetNativeDevice(nativeDevice); diff --git a/code/components/vfs-impl-server/include/Manager.h b/code/components/vfs-impl-server/include/Manager.h index 2a9b938cd2..31bbc76731 100644 --- a/code/components/vfs-impl-server/include/Manager.h +++ b/code/components/vfs-impl-server/include/Manager.h @@ -8,11 +8,19 @@ namespace vfs class ManagerServer : public vfs::Manager { private: + struct MountedDevice + { + fwRefContainer device; + std::string absolutePath; + std::string canonicalPath; + bool resolvesThroughLink = false; + }; + struct MountPoint { std::string prefix; - mutable std::vector> devices; // mutable? MUTABLE? is this Rust?! + mutable std::vector devices; // mutable? MUTABLE? is this Rust?! }; // sorts mount points based on longest prefix @@ -37,6 +45,8 @@ class ManagerServer : public vfs::Manager std::recursive_mutex m_mountMutex; + bool m_hasCanonicalPathMounts = false; + // fallback device - usually a local file system implementation fwRefContainer m_fallbackDevice; @@ -50,6 +60,8 @@ class ManagerServer : public vfs::Manager virtual fwRefContainer GetNativeDevice(void* nativeDevice) override; + virtual fwRefContainer FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string& preferredMountPrefix) override; + virtual void Mount(fwRefContainer device, const std::string& path) override; virtual void Unmount(const std::string& path) override; diff --git a/code/components/vfs-impl-server/src/Manager.cpp b/code/components/vfs-impl-server/src/Manager.cpp index aa23a1b3a3..109048ed38 100644 --- a/code/components/vfs-impl-server/src/Manager.cpp +++ b/code/components/vfs-impl-server/src/Manager.cpp @@ -4,6 +4,143 @@ #include #include +#include + +namespace +{ +struct DeviceMatch +{ + size_t matchedPrefixLength = 0; + size_t canonicalPrefixLength = 0; + fwRefContainer device {nullptr}; + std::string transformedPath; + bool isPreferredPrefix = false; +}; + +enum class DevicePathKind +{ + Lexical, + Canonical +}; + +std::string GetLexicalAbsoluteGenericPath(const std::filesystem::path& path) +{ + std::error_code ec; + std::filesystem::path absolutePath = std::filesystem::absolute(path, ec); + if (ec) + { + absolutePath = path; + } + + return absolutePath.lexically_normal().generic_string(); +} + +std::string GetCanonicalAbsoluteGenericPath(const std::filesystem::path& path) +{ + std::error_code ec; + std::filesystem::path normalizedPath = std::filesystem::weakly_canonical(path, ec); + if (ec) + { + return GetLexicalAbsoluteGenericPath(path); + } + + return normalizedPath.lexically_normal().generic_string(); +} + +void AddDirectorySuffix(std::string& path, const std::filesystem::path& sourcePath) +{ + if (path.empty() || path.back() == '/') + { + return; + } + + std::string sourcePathString = sourcePath.generic_string(); + if (!sourcePathString.empty() && sourcePathString.back() == '/') + { + path += '/'; + return; + } + + std::error_code ec; + if (std::filesystem::is_directory(sourcePath, ec) && !ec) + { + path += '/'; + } +} + +std::string GetMountedDevicePath(const std::string& devicePath, bool canonical) +{ + if (devicePath.empty()) + { + return {}; + } + + std::filesystem::path path(devicePath); + std::string absolutePath = canonical + ? GetCanonicalAbsoluteGenericPath(path) + : GetLexicalAbsoluteGenericPath(path); + AddDirectorySuffix(absolutePath, path); + return absolutePath; +} + +void UpdateDeviceMatch(DeviceMatch& match, size_t matchedPrefixLength, size_t canonicalPrefixLength, + const fwRefContainer& device, std::string transformedPath, const bool isPreferredPrefix) +{ + if (match.device.GetRef()) + { + if (match.matchedPrefixLength > matchedPrefixLength) + { + return; + } + + if (match.matchedPrefixLength == matchedPrefixLength && match.isPreferredPrefix && !isPreferredPrefix) + { + return; + } + } + + match.matchedPrefixLength = matchedPrefixLength; + match.canonicalPrefixLength = canonicalPrefixLength; + match.device = device; + match.transformedPath = std::move(transformedPath); + match.isPreferredPrefix = isPreferredPrefix; +} + +const DeviceMatch& SelectDeviceMatch(const DeviceMatch& lexicalMatch, const DeviceMatch& canonicalMatch, const bool pathResolvedThroughLink) +{ + if (!lexicalMatch.device.GetRef()) + { + return canonicalMatch; + } + + if (!canonicalMatch.device.GetRef()) + { + return lexicalMatch; + } + + // Keep VFS longest-prefix semantics after symlink resolution: a more specific + // canonical mount wins even when the preferred mount is a parent. + if (lexicalMatch.canonicalPrefixLength != canonicalMatch.canonicalPrefixLength) + { + return lexicalMatch.canonicalPrefixLength > canonicalMatch.canonicalPrefixLength + ? lexicalMatch + : canonicalMatch; + } + + if (pathResolvedThroughLink) + { + return lexicalMatch; + } + + if (canonicalMatch.isPreferredPrefix && !lexicalMatch.isPreferredPrefix) + { + return canonicalMatch; + } + + return lexicalMatch; +} + +} namespace vfs { @@ -16,55 +153,104 @@ fwRefContainer MakeMemoryDevice(); fwRefContainer ManagerServer::FindDevice(const std::string& absolutePath, std::string& transformedPath) { + return FindDevice(absolutePath, transformedPath, {}); +} + +fwRefContainer ManagerServer::FindDevice(const std::string& absolutePath, std::string& transformedPath, const std::string& preferredMountPrefix) +{ + transformedPath.clear(); + std::filesystem::path absolute(absolutePath); - std::string absoluteGenericString = std::filesystem::absolute(absolute).generic_string(); - std::lock_guard lock(m_mountMutex); + std::string absoluteGenericString = GetLexicalAbsoluteGenericPath(absolute); + AddDirectorySuffix(absoluteGenericString, absolute); - // ensure directories have a / suffix - // e.g. /usr/local should be matched for /usr/local/ - if (!absoluteGenericString.empty() && absoluteGenericString.back() != '/') + auto findDevice = [&](const std::string& path, const DevicePathKind pathKind) { - std::error_code ec; - if (std::filesystem::is_directory(absoluteGenericString, ec) && !ec) + DeviceMatch foundMatch; + + for (const auto& mount : m_mounts) { - absoluteGenericString += '/'; + for (const auto& mountedDevice : mount.devices) + { + const std::string& deviceAbsolutePath = pathKind == DevicePathKind::Canonical + ? mountedDevice.canonicalPath + : mountedDevice.absolutePath; + if (deviceAbsolutePath.empty()) + { + continue; + } + + size_t prefixLength = deviceAbsolutePath.size(); + + // device matches if the path start is the same + // e.g. usr/local/bin starts with /usr/local/ + if (path.rfind(deviceAbsolutePath, 0) == 0) + { + const bool isPreferredPrefix = !preferredMountPrefix.empty() && mount.prefix == preferredMountPrefix; + std::string nextTransformedPath = mount.prefix + path.substr(prefixLength); + size_t canonicalPrefixLength = prefixLength; + if (pathKind == DevicePathKind::Lexical && + mountedDevice.resolvesThroughLink && + !mountedDevice.canonicalPath.empty()) + { + canonicalPrefixLength = mountedDevice.canonicalPath.size(); + } + + // find the device with the largest prefix length + UpdateDeviceMatch(foundMatch, prefixLength, canonicalPrefixLength, mountedDevice.device, + std::move(nextTransformedPath), isPreferredPrefix); + } + } } - } - size_t longestPrefixLength = 0; - fwRefContainer foundDevice {nullptr}; - for (const auto& mount : m_mounts) + return foundMatch; + }; + + auto returnMatch = [&](DeviceMatch& match) { - for (const auto& device : mount.devices) + if (match.device.GetRef()) { - // check if the device has an absolute path - if (device->GetAbsolutePath().empty()) - { - continue; - } + transformedPath = std::move(match.transformedPath); + } - std::string deviceAbsolutePath = std::filesystem::absolute(std::filesystem::path(device->GetAbsolutePath())).generic_string(); - size_t prefixLength = deviceAbsolutePath.size(); + return match.device; + }; - // find the device with the largest prefix length - if (longestPrefixLength > prefixLength) - { - continue; - } + { + std::lock_guard lock(m_mountMutex); + DeviceMatch lexicalMatch = findDevice(absoluteGenericString, DevicePathKind::Lexical); - // device matches if the path start is the same - // e.g. usr/local/bin starts with /usr/local/ - if (absoluteGenericString.rfind(deviceAbsolutePath, 0) == 0) - { - longestPrefixLength = deviceAbsolutePath.size(); - foundDevice = device; - // e.g. /usr/local/bin -> bin, @local/bin - transformedPath = mount.prefix + absoluteGenericString.substr(deviceAbsolutePath.size()); - } + if (lexicalMatch.device.GetRef() && (preferredMountPrefix.empty() || lexicalMatch.isPreferredPrefix)) + { + return returnMatch(lexicalMatch); + } + + if (!m_hasCanonicalPathMounts) + { + return returnMatch(lexicalMatch); } } - return foundDevice; + std::string canonicalGenericString = GetCanonicalAbsoluteGenericPath(absolute); + AddDirectorySuffix(canonicalGenericString, absolute); + const bool pathResolvedThroughLink = absoluteGenericString != canonicalGenericString; + + std::lock_guard lock(m_mountMutex); + DeviceMatch lexicalMatch = findDevice(absoluteGenericString, DevicePathKind::Lexical); + if (lexicalMatch.device.GetRef() && (preferredMountPrefix.empty() || lexicalMatch.isPreferredPrefix)) + { + return returnMatch(lexicalMatch); + } + + if (!m_hasCanonicalPathMounts) + { + return returnMatch(lexicalMatch); + } + + DeviceMatch canonicalMatch = findDevice(canonicalGenericString, DevicePathKind::Canonical); + DeviceMatch selectedMatch = SelectDeviceMatch(lexicalMatch, canonicalMatch, pathResolvedThroughLink); + + return returnMatch(selectedMatch); } fwRefContainer ManagerServer::GetDevice(const std::string& path) @@ -92,15 +278,16 @@ fwRefContainer ManagerServer::GetDevice(const std::string& path) // single device case if (mount.devices.size() == 1) { - return mount.devices[0]; + return mount.devices[0].device; } else { // check each device assigned to the mount point Device::THandle handle; - for (const auto& device : mount.devices) + for (const auto& mountedDevice : mount.devices) { + const auto& device = mountedDevice.device; if ((handle = device->Open(path, true)) != Device::InvalidHandle) { device->Close(handle); @@ -130,6 +317,17 @@ void ManagerServer::Mount(fwRefContainer device, const std::string& path // set the path prefix on the device device->SetPathPrefix(path); + const std::string devicePath = device->GetAbsolutePath(); + const std::string absolutePath = GetMountedDevicePath(devicePath, false); + std::string canonicalPath = GetMountedDevicePath(absolutePath, true); + const bool resolvesThroughLink = !canonicalPath.empty() && canonicalPath != absolutePath; + MountedDevice mountedDevice { + device, + absolutePath, + std::move(canonicalPath), + resolvesThroughLink + }; + // mount std::lock_guard lock(m_mountMutex); @@ -138,7 +336,8 @@ void ManagerServer::Mount(fwRefContainer device, const std::string& path { if (mount.prefix == path) { - mount.devices.push_back(device); + mount.devices.push_back(mountedDevice); + m_hasCanonicalPathMounts = m_hasCanonicalPathMounts || resolvesThroughLink; return; } } @@ -147,9 +346,10 @@ void ManagerServer::Mount(fwRefContainer device, const std::string& path { MountPoint mount; mount.prefix = path; - mount.devices.push_back(device); + mount.devices.push_back(mountedDevice); m_mounts.insert(mount); + m_hasCanonicalPathMounts = m_hasCanonicalPathMounts || resolvesThroughLink; } } @@ -171,6 +371,19 @@ void ManagerServer::Unmount(const std::string& path) ++it; } } + + m_hasCanonicalPathMounts = false; + for (const auto& mount : m_mounts) + { + for (const auto& mountedDevice : mount.devices) + { + m_hasCanonicalPathMounts = m_hasCanonicalPathMounts || mountedDevice.resolvesThroughLink; + if (m_hasCanonicalPathMounts) + { + return; + } + } + } } } From 085adf7ebc124f1c711b1cc739802c2fc639c1e9 Mon Sep 17 00:00:00 2001 From: gtolontop Date: Sat, 2 May 2026 19:12:17 +0200 Subject: [PATCH 2/3] fix(scripting/node): prefer resource mount in fs sandbox --- .../citizen-scripting-node/include/NodeScriptRuntime.h | 1 + .../citizen-scripting-node/src/NodeScriptRuntime.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/code/components/citizen-scripting-node/include/NodeScriptRuntime.h b/code/components/citizen-scripting-node/include/NodeScriptRuntime.h index 341cc74eb1..99568d2377 100644 --- a/code/components/citizen-scripting-node/include/NodeScriptRuntime.h +++ b/code/components/citizen-scripting-node/include/NodeScriptRuntime.h @@ -52,6 +52,7 @@ class NodeScriptRuntime : public OMClassGetResourceName(&resourceName); m_resourceName = resourceName; + m_resourceMountPrefix = "@" + m_resourceName + "/"; // don't create the runtime environment if nodejs is not initialized if (!g_nodeEnv.IsInitialized()) From b13f758cf806ce83f59fd5e5cca854a7c59e829a Mon Sep 17 00:00:00 2001 From: gtolontop Date: Sat, 2 May 2026 19:12:22 +0200 Subject: [PATCH 3/3] test(server): cover symlinked resource vfs lookup --- code/tests/server/TestLua.cpp | 239 +++++++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 1 deletion(-) diff --git a/code/tests/server/TestLua.cpp b/code/tests/server/TestLua.cpp index a25e184995..2ceab35b65 100644 --- a/code/tests/server/TestLua.cpp +++ b/code/tests/server/TestLua.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -180,7 +181,54 @@ void LoadAndRunCode(fx::LuaStateHolder& state, const std::string&& fileName, con REQUIRE(!expectExecutionError); } } -} + +template +class ScopeExit +{ +public: + explicit ScopeExit(TCallback callback) + : m_callback(std::move(callback)) + { + } + + ~ScopeExit() + { + m_callback(); + } + + ScopeExit(const ScopeExit&) = delete; + ScopeExit& operator=(const ScopeExit&) = delete; + +private: + TCallback m_callback; +}; + +template +ScopeExit(TCallback) -> ScopeExit; + +void CreateDirectorySymlinkOrSkip(const std::filesystem::path& target, const std::filesystem::path& link, const char* description) +{ + std::error_code ec; + std::filesystem::create_directory_symlink(target, link, ec); + if (ec) + { + SKIP("Could not " << description << ": " << ec.message()); + } +} + +std::filesystem::path GetNodePermissionPath(const std::filesystem::path& path) +{ + std::error_code ec; + std::filesystem::path normalizedPath = std::filesystem::weakly_canonical(path, ec); + if (!ec) + { + return normalizedPath; + } + + normalizedPath = std::filesystem::absolute(path, ec); + return ec ? path : normalizedPath; +} +} // todo: emulate threading @@ -926,6 +974,195 @@ TEST_CASE("vfs") } } +#ifdef IS_FXSERVER +TEST_CASE("vfs resolves symlinked resource paths for node sandbox permission checks") +{ + REQUIRE(Instance::Get() != nullptr); + + std::error_code ec; + const auto basePath = std::filesystem::current_path() / "vfs_symlink_test"; + const auto targetPath = basePath / "target"; + const auto nestedTargetPath = targetPath / "nested"; + const auto shortTargetPath = basePath / "t"; + const auto canonicalTargetPath = basePath / "canonical_target"; + const auto linkPath = basePath / "resource_link"; + const auto duplicateLinkPath = basePath / "resource_link_duplicate"; + const auto canonicalLinkPath = basePath / "resource_canonical_link"; + const auto shortNestedLinkPath = targetPath / "short_link"; + const auto nestedLinkPath = basePath / "resource_nested_link"; + const std::string mountPath = "@symlink-test/"; + const std::string duplicateMountPath = "@symlink-test-duplicate/"; + const std::string directMountPath = "@symlink-test-direct/"; + const std::string canonicalMountPath = "@symlink-test-canonical/"; + const std::string shortNestedMountPath = "@symlink-test-short-nested/"; + const std::string nestedMountPath = "@symlink-test-nested/"; + + ScopeExit cleanup([&]() + { + std::error_code cleanupEc; + vfs::Unmount(nestedMountPath); + vfs::Unmount(shortNestedMountPath); + vfs::Unmount(canonicalMountPath); + vfs::Unmount(directMountPath); + vfs::Unmount(duplicateMountPath); + vfs::Unmount(mountPath); + std::filesystem::remove_all(basePath, cleanupEc); + }); + + std::filesystem::remove_all(basePath, ec); + ec.clear(); + REQUIRE(std::filesystem::create_directories(nestedTargetPath, ec)); + REQUIRE(!ec); + ec.clear(); + REQUIRE(std::filesystem::create_directories(shortTargetPath, ec)); + REQUIRE(!ec); + ec.clear(); + REQUIRE(std::filesystem::create_directories(canonicalTargetPath, ec)); + REQUIRE(!ec); + + CreateDirectorySymlinkOrSkip(targetPath, linkPath, "create directory symlink"); + + fwRefContainer relativeDevice = new vfs::RelativeDevice(linkPath.generic_string() + "/"); + vfs::Unmount(mountPath); + vfs::Mount(relativeDevice, mountPath); + + fwRefContainer stream = vfs::Create(mountPath + "module.cjs", false); + REQUIRE(stream.GetRef() != nullptr); + stream->Close(); + + std::string transformedPath; + const auto linkFile = linkPath / "module.cjs"; + const auto linkDevice = vfs::FindDevice(linkFile.string(), transformedPath); + REQUIRE(linkDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "module.cjs"); + + transformedPath.clear(); + // Mirrors Node's sandbox callback after require() resolves the symlinked resource path. + const auto nodeRequirePath = GetNodePermissionPath(linkFile); + const auto targetFile = targetPath / "module.cjs"; + const auto nodeRequireDevice = vfs::FindDevice(nodeRequirePath.string(), transformedPath, mountPath); + REQUIRE(nodeRequireDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "module.cjs"); + + transformedPath.clear(); + const auto targetDirectoryDevice = vfs::FindDevice(targetPath.string(), transformedPath, mountPath); + REQUIRE(targetDirectoryDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath); + + CreateDirectorySymlinkOrSkip(shortTargetPath, shortNestedLinkPath, "create nested lexical directory symlink"); + + fwRefContainer shortNestedDevice = new vfs::RelativeDevice((linkPath / "short_link").generic_string() + "/"); + vfs::Unmount(shortNestedMountPath); + vfs::Mount(shortNestedDevice, shortNestedMountPath); + + transformedPath.clear(); + const auto shortNestedFile = linkPath / "short_link" / "module.cjs"; + const auto lexicalLongestDevice = vfs::FindDevice(shortNestedFile.string(), transformedPath); + REQUIRE(lexicalLongestDevice.GetRef() == shortNestedDevice.GetRef()); + REQUIRE(transformedPath == shortNestedMountPath + "module.cjs"); + + CreateDirectorySymlinkOrSkip(targetPath, duplicateLinkPath, "create duplicate directory symlink"); + + fwRefContainer duplicateDevice = new vfs::RelativeDevice(duplicateLinkPath.generic_string() + "/"); + vfs::Unmount(duplicateMountPath); + vfs::Mount(duplicateDevice, duplicateMountPath); + + transformedPath.clear(); + const auto lexicalLinkDevice = vfs::FindDevice(linkFile.string(), transformedPath, duplicateMountPath); + REQUIRE(lexicalLinkDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "module.cjs"); + + transformedPath.clear(); + const auto preferredTargetDevice = vfs::FindDevice(targetFile.string(), transformedPath, duplicateMountPath); + REQUIRE(preferredTargetDevice.GetRef() == duplicateDevice.GetRef()); + REQUIRE(transformedPath == duplicateMountPath + "module.cjs"); + + transformedPath.clear(); + const auto preferredOriginalDevice = vfs::FindDevice(targetFile.string(), transformedPath, mountPath); + REQUIRE(preferredOriginalDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "module.cjs"); + + fwRefContainer directDevice = new vfs::RelativeDevice(targetPath.generic_string() + "/"); + vfs::Unmount(directMountPath); + vfs::Mount(directDevice, directMountPath); + + transformedPath.clear(); + const auto canonicalPreferredDevice = vfs::FindDevice(targetFile.string(), transformedPath, mountPath); + REQUIRE(canonicalPreferredDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "module.cjs"); + + transformedPath.clear(); + const auto directPreferredDevice = vfs::FindDevice(targetFile.string(), transformedPath, directMountPath); + REQUIRE(directPreferredDevice.GetRef() == directDevice.GetRef()); + REQUIRE(transformedPath == directMountPath + "module.cjs"); + + transformedPath.clear(); + const auto newTargetFile = linkPath / "generated" / "write.js"; + const auto newTargetDevice = vfs::FindDevice(GetNodePermissionPath(newTargetFile).string(), transformedPath, mountPath); + REQUIRE(newTargetDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "generated/write.js"); + + transformedPath.clear(); + const auto newTargetDirectory = linkPath / "generated-directory"; + const auto newTargetDirectoryDevice = vfs::FindDevice(GetNodePermissionPath(newTargetDirectory).string(), transformedPath, mountPath); + REQUIRE(newTargetDirectoryDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "generated-directory"); + + transformedPath.clear(); + const auto newTargetDirectoryWithSlash = (linkPath / "generated-directory-with-slash").generic_string() + "/"; + const auto newTargetDirectoryWithSlashDevice = vfs::FindDevice(newTargetDirectoryWithSlash, transformedPath, mountPath); + REQUIRE(newTargetDirectoryWithSlashDevice.GetRef() == relativeDevice.GetRef()); + REQUIRE(transformedPath == mountPath + "generated-directory-with-slash/"); + + CreateDirectorySymlinkOrSkip(canonicalTargetPath, canonicalLinkPath, "create canonical directory symlink"); + + fwRefContainer canonicalDevice = new vfs::RelativeDevice(canonicalLinkPath.generic_string() + "/"); + vfs::Unmount(canonicalMountPath); + vfs::Mount(canonicalDevice, canonicalMountPath); + + transformedPath.clear(); + const auto canonicalFile = canonicalTargetPath / "module.cjs"; + const auto canonicalDeviceMatch = vfs::FindDevice(canonicalFile.string(), transformedPath, canonicalMountPath); + REQUIRE(canonicalDeviceMatch.GetRef() == canonicalDevice.GetRef()); + REQUIRE(transformedPath == canonicalMountPath + "module.cjs"); + + CreateDirectorySymlinkOrSkip(nestedTargetPath, nestedLinkPath, "create nested directory symlink"); + + fwRefContainer nestedDevice = new vfs::RelativeDevice(nestedLinkPath.generic_string() + "/"); + vfs::Unmount(nestedMountPath); + vfs::Mount(nestedDevice, nestedMountPath); + + transformedPath.clear(); + const auto nestedTargetFile = nestedTargetPath / "write.js"; + const auto lexicalTargetDevice = vfs::FindDevice(nestedTargetFile.string(), transformedPath); + REQUIRE(lexicalTargetDevice.GetRef() == directDevice.GetRef()); + REQUIRE(transformedPath == directMountPath + "nested/write.js"); + + transformedPath.clear(); + const auto preferredParentResolvedDevice = vfs::FindDevice(nestedTargetFile.string(), transformedPath, mountPath); + REQUIRE(preferredParentResolvedDevice.GetRef() == nestedDevice.GetRef()); + REQUIRE(transformedPath == nestedMountPath + "write.js"); + + transformedPath.clear(); + const auto preferredNestedDevice = vfs::FindDevice(nestedTargetFile.string(), transformedPath, nestedMountPath); + REQUIRE(preferredNestedDevice.GetRef() == nestedDevice.GetRef()); + REQUIRE(transformedPath == nestedMountPath + "write.js"); + + const auto siblingPath = basePath / "target-other"; + REQUIRE(std::filesystem::create_directories(siblingPath, ec)); + REQUIRE(!ec); + transformedPath = "stale"; + const auto siblingDevice = vfs::FindDevice((siblingPath / "module.cjs").string(), transformedPath); + REQUIRE(siblingDevice.GetRef() == nullptr); + REQUIRE(transformedPath.empty()); + + transformedPath = "stale"; + const auto siblingPreferredDevice = vfs::FindDevice((siblingPath / "module.cjs").string(), transformedPath, mountPath); + REQUIRE(siblingPreferredDevice.GetRef() == nullptr); + REQUIRE(transformedPath.empty()); +} +#endif + TEST_CASE("debug namespace") { WHEN ("debug.getinfo is used")