From 1ea481bea0578aa50c65ae3495a5df0365d91c2b Mon Sep 17 00:00:00 2001 From: JainAyush-01 Date: Thu, 22 Oct 2026 11:16:21 +0000 Subject: [PATCH] [core] Fix plugin lookup when Root.PluginPath has multiple directories When Root.PluginPath (or ROOT_PLUGIN_PATH) contains multiple colon-separated directories, TPluginManager::LoadHandlersFromPluginDirs loops over each directory to load handler macros for the requested base class. In commit e60640c52a (#18859), ConcatFileName was replaced by PrependPathName. Unlike ConcatFileName which returned a new heap-allocated string, PrependPathName modifies its second parameter in-place. Because `sbase` was passed directly: const char *p = gSystem->PrependPathName(d, sbase); the variable `sbase` was mutated in-place across iterations (e.g., from "TArchiveFile" to "/dir1/TArchiveFile", then to "/dir2//dir1/TArchiveFile"). Consequently, subsequent search directories received a mangled path and ROOT could not find its own built-in plugins whenever a custom plugin directory was prepended. Fix this by copying `sbase` to a temporary `TString temp = sbase;` on each iteration, matching the pattern used in the rest of TPluginManager.cxx. Fixes #23536. Added regression test in core/base/test/TPluginManagerTests.cxx. --- core/base/src/TPluginManager.cxx | 3 +- core/base/test/CMakeLists.txt | 2 + core/base/test/TPluginManagerTests.cxx | 73 ++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 core/base/test/TPluginManagerTests.cxx diff --git a/core/base/src/TPluginManager.cxx b/core/base/src/TPluginManager.cxx index 9a20d9207be80..7e4e71ae2cc91 100644 --- a/core/base/src/TPluginManager.cxx +++ b/core/base/src/TPluginManager.cxx @@ -532,7 +532,8 @@ void TPluginManager::LoadHandlersFromPluginDirs(const char *base) } if (!skip) { if (sbase != "") { - const char *p = gSystem->PrependPathName(d, sbase); + TString temp = sbase; + const char *p = gSystem->PrependPathName(d, temp); LoadHandlerMacros(p); } else { void *dirp = gSystem->OpenDirectory(d); diff --git a/core/base/test/CMakeLists.txt b/core/base/test/CMakeLists.txt index c60aa85eb9992..9528a5c1af67a 100644 --- a/core/base/test/CMakeLists.txt +++ b/core/base/test/CMakeLists.txt @@ -36,3 +36,5 @@ configure_file(Foo.C Foo.C COPYONLY) ROOT_ADD_GTEST(IncludePathTest IncludePathTest.cxx LIBRARIES Core) ROOT_ADD_GTEST(UUIDTest UUIDTest.cxx LIBRARIES Core) + +ROOT_ADD_GTEST(CorePluginManagerTests TPluginManagerTests.cxx LIBRARIES Core) diff --git a/core/base/test/TPluginManagerTests.cxx b/core/base/test/TPluginManagerTests.cxx new file mode 100644 index 0000000000000..848db82804630 --- /dev/null +++ b/core/base/test/TPluginManagerTests.cxx @@ -0,0 +1,73 @@ +#include "gtest/gtest.h" + +#include "TPluginManager.h" +#include "TROOT.h" +#include "TEnv.h" +#include "TSystem.h" +#include "TUUID.h" + +#include + +// Regression test for https://github.com/root-project/root/issues/23536: +// TPluginManager::FindHandler must correctly resolve plugins across all directories +// when Root.PluginPath contains multiple colon-separated directory paths. +TEST(TPluginManager, MultiDirectoryPluginPath) +{ + // Save original Root.PluginPath + const char *origPluginPath = gEnv->GetValue("Root.PluginPath", (char *)nullptr); + TString savedPath = origPluginPath ? origPluginPath : ""; + + // Construct default plugin path ($ROOTSYS/etc/plugins) + TString defaultPath = "plugins"; + gSystem->PrependPathName(TROOT::GetEtcDir(), defaultPath); + + // Create a temporary custom plugin directory with a custom macro + TUUID uuid; + TString tempDir = gSystem->TempDirectory(); + TString customDir = TString::Format("test_plugins_%s", uuid.AsString()); + gSystem->PrependPathName(tempDir.Data(), customDir); + + TString baseSubdir = "TCustomPluginBase"; + gSystem->PrependPathName(customDir.Data(), baseSubdir); + ASSERT_EQ(0, gSystem->mkdir(baseSubdir.Data(), /*recursive=*/true)); + + TString macroFile = "P010_TCustomPlugin.C"; + gSystem->PrependPathName(baseSubdir.Data(), macroFile); + + { + std::ofstream f(macroFile.Data()); + ASSERT_TRUE(f.is_open()); + f << "void P010_TCustomPlugin()\n" + << "{\n" + << " gPluginMgr->AddHandler(\"TCustomPluginBase\", \".*\", \"TCustomClass\",\n" + << " \"CustomLib\", \"TCustomClass()\");\n" + << "}\n"; + } + + // Set multi-directory Root.PluginPath: custom directory prepended before default directory + TString multiPath = customDir + ":" + defaultPath; + gEnv->SetValue("Root.PluginPath", multiPath.Data()); + + // 1. Verify that the custom plugin from the first directory is resolved + TPluginHandler *hCustom = gPluginMgr->FindHandler("TCustomPluginBase", "custom://test"); + ASSERT_NE(hCustom, nullptr); + EXPECT_STREQ(hCustom->GetClass(), "TCustomClass"); + + // 2. Verify that built-in plugins from subsequent directories are resolved properly (not dropped or corrupted) + TPluginHandler *hArchive = gPluginMgr->FindHandler("TArchiveFile", "test.zip"); + ASSERT_NE(hArchive, nullptr); + EXPECT_STREQ(hArchive->GetClass(), "TZIPFile"); + + TPluginHandler *hSQL = gPluginMgr->FindHandler("TSQLServer", "sqlite://test.db"); + ASSERT_NE(hSQL, nullptr); + EXPECT_STREQ(hSQL->GetClass(), "TSQLiteServer"); + + // Clean up + gSystem->Unlink(macroFile.Data()); + gSystem->Unlink(baseSubdir.Data()); + gSystem->Unlink(customDir.Data()); + + if (!savedPath.IsNull()) { + gEnv->SetValue("Root.PluginPath", savedPath.Data()); + } +}