From 3379b33c43adb49ee6f004fbc8eb960c035bbfa1 Mon Sep 17 00:00:00 2001 From: Prakhar Singh Date: Sat, 30 May 2026 19:47:14 +0530 Subject: [PATCH] [io] fix TFileMerger kOnlyListed suffix key leak An unlisted key whose name is a prefix or suffix of a listed key was incorrectly included in the merged output, because the name check was a plain substring search with no leading word boundary. Fixes https://github.com/root-project/root/issues/22414 --- io/io/src/TFileMerger.cxx | 11 ++++++---- io/io/test/TFileMergerTests.cxx | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/io/io/src/TFileMerger.cxx b/io/io/src/TFileMerger.cxx index b0eccc6d4b37b..727bc530701a0 100644 --- a/io/io/src/TFileMerger.cxx +++ b/io/io/src/TFileMerger.cxx @@ -498,10 +498,13 @@ Bool_t TFileMerger::MergeOne(TDirectory *target, TList *sourcelist, Int_t type, } // Check if only the listed objects are to be merged if (type & kOnlyListed) { - oldkeyname = keyname; - oldkeyname += " "; - onlyListed = fObjectNames.Contains(oldkeyname); - oldkeyname = keyname; + // Search for " key " in " a b c " to match whole words only. + // Without the leading space, a key that is a prefix or suffix of a listed name + // would match. AddObjectNames() guarantees a trailing space in fObjectNames. + TString searchName = " "; + searchName += keyname; + searchName += " "; + onlyListed = (" " + fObjectNames).Contains(searchName); if ((!onlyListed) && (!cl->InheritsFrom(TDirectory::Class()))) return kTRUE; } diff --git a/io/io/test/TFileMergerTests.cxx b/io/io/test/TFileMergerTests.cxx index db58eedb1a209..137bf1f89bc34 100644 --- a/io/io/test/TFileMergerTests.cxx +++ b/io/io/test/TFileMergerTests.cxx @@ -118,6 +118,44 @@ TEST(TFileMerger, MergeSingleOnlyListed) EXPECT_EQ(output->GetListOfKeys()->GetSize(), 2); } +TEST(TFileMerger, OnlyListedNoSuffixLeak) +{ + // Regression test for https://github.com/root-project/root/issues/22414: + // keys whose names are a prefix or suffix of a listed key must not appear in the output. + TMemFile src("OnlyListedNoSuffixLeakSrc.root", "CREATE"); + + // "short" is a suffix of "long_short"; "long" is a prefix — both must be excluded. + auto hLongShort = new TH1F("long_short", "long_short", 1, 0, 2); + auto hShort = new TH1F("short", "short", 1, 0, 2); + auto hLong = new TH1F("long", "long", 1, 0, 2); + auto hUnrelated = new TH1F("unrelated", "unrelated", 1, 0, 2); + for (auto h : {hLongShort, hShort, hLong, hUnrelated}) + h->SetDirectory(&src); + src.Write(); + + TFileMerger merger; + auto output = std::unique_ptr(new TFile("OnlyListedNoSuffixLeak.root", "RECREATE")); + ASSERT_TRUE(merger.OutputFile(std::move(output))); + + merger.AddObjectNames("long_short"); // only this one should appear in output + merger.AddFile(&src, false); + + const Int_t mode = TFileMerger::kAll | TFileMerger::kRegular | TFileMerger::kOnlyListed; + ASSERT_TRUE(merger.PartialMerge(mode)); + + output = std::unique_ptr(TFile::Open("OnlyListedNoSuffixLeak.root")); + ASSERT_TRUE(output.get() && output->GetListOfKeys()); + + // Exactly one key: "long_short". Suffix, prefix, and unrelated keys must be absent. + EXPECT_EQ(output->GetListOfKeys()->GetSize(), 1); + EXPECT_NE(output->Get("long_short"), nullptr); + EXPECT_EQ(output->Get("short"), nullptr); + EXPECT_EQ(output->Get("long"), nullptr); + EXPECT_EQ(output->Get("unrelated"), nullptr); + output->Close(); + gSystem->Unlink("OnlyListedNoSuffixLeak.root"); +} + // https://github.com/root-project/root/issues/14558 aka https://its.cern.ch/jira/browse/ROOT-4716 TEST(TFileMerger, MergeBranches) {