Skip to content

TFileMerger: when using kOnlyListed, any 'listed' key will bring in any unlisted keys that are a suffix of a listed key #22414

@ryuwd

Description

@ryuwd

Check duplicate issues.

  • Checked for duplicates

Description

Potentially falls under the umbrella of the title of #19330 but I'd say this is not a duplicate issue


When merging with TFileMerger::PartialMerge (or Merge) using the
kOnlyListed flag together with AddObjectNames(...), the merger keeps not
only the listed objects but also any unlisted top-level key whose name is a
suffix of one of the listed names.

The name test is a plain substring search that requires a trailing delimiter
but no leading delimiter, so "D0ToKmPip" is treated as listed whenever
"DstpToD0Pip_D0ToKmPip" is listed.

example

File keys: k1, k2, ak2, k3, bk3. We list only ak2 and bk3
(fObjectNames == "ak2 bk3 "). The merger tests each file key:

file key test result outcome
k1 "ak2 bk3 ".Contains("k1 ") false dropped (correct)
k2 "ak2 bk3 ".Contains("k2 ") true kept (wrong) — suffix of ak2
ak2 "ak2 bk3 ".Contains("ak2 ") true kept (correct)
k3 "ak2 bk3 ".Contains("k3 ") true kept (wrong) — suffix of bk3
bk3 "ak2 bk3 ".Contains("bk3 ") true kept (correct)

Output: k2, ak2, k3, bk3 instead of ak2, bk3.

Reproducer

#!/usr/bin/env python
"""Minimal reproducer for the TFileMerger kOnlyListed suffix-match bug.

Run with a ROOT-enabled Python, e.g.:

    python tfilemerger-onlylisted-repro.py

The script builds a source file with three top-level TDirectories, asks
TFileMerger to keep only ONE of them via kOnlyListed + AddObjectNames, and
then checks what actually ended up in the output.

A non-listed directory whose name is a *suffix* of the listed one leaks into
the output, because the name matching is a substring test with only a
trailing delimiter and no leading delimiter.
"""

import array
import sys

import ROOT

SRC = "repro_src.root"
OUT = "repro_out.root"

KEEP = "DstpToD0Pip_D0ToKmPip"  # the only directory we ask to keep
LEAK = "D0ToKmPip"  # NOT listed, but a suffix of KEEP -> leaks
CONTROL = "Unrelated"  # NOT listed, not a suffix -> correctly dropped


def make_dir_with_tree(rf, dname):
    """Create a non-empty top-level TDirectory holding a tiny TTree."""
    d = rf.mkdir(dname)
    d.cd()
    t = ROOT.TTree("DecayTree", "DecayTree")
    val = array.array("i", [0])
    t.Branch("val", val, "val/I")
    for i in range(3):
        val[0] = i
        t.Fill()
    t.Write()
    rf.cd()


def main():
    rf = ROOT.TFile.Open(SRC, "RECREATE")
    make_dir_with_tree(rf, KEEP)
    make_dir_with_tree(rf, LEAK)
    make_dir_with_tree(rf, CONTROL)
    rf.Close()

    merger = ROOT.TFileMerger(False, False)
    merger.SetPrintLevel(0)
    merger.AddFile(SRC)
    merger.AddObjectNames(KEEP)  # keep only this one
    merge_opts = (
        ROOT.TFileMerger.kAll | ROOT.TFileMerger.kRegular | ROOT.TFileMerger.kOnlyListed
    )
    merger.OutputFile(OUT, "RECREATE")
    ok = merger.PartialMerge(merge_opts)
    merger.CloseOutputFile()
    del merger

    of = ROOT.TFile.Open(OUT)
    keys = sorted(k.GetName() for k in of.GetListOfKeys())
    of.Close()

    print(f"ROOT version      : {ROOT.gROOT.GetVersion()}")
    print(f"PartialMerge ok   : {bool(ok)}")
    print(f"Listed to keep    : [{KEEP!r}]")
    print(f"Output top-level  : {keys}")
    print(f"Expected          : [{KEEP!r}]")

    leaked = LEAK in keys
    control_dropped = CONTROL not in keys
    print()
    print(f"BUG: suffix dir {LEAK!r} leaked : {leaked}")
    print(f"control {CONTROL!r} dropped       : {control_dropped}")

    return 1 if leaked else 0


if __name__ == "__main__":
    sys.exit(main())

Expected output:

[roneil@lblhcbpr20 ~]$ pixi exec -s root python root-issue-22414.py
ROOT version      : 6.38.04
PartialMerge ok   : True
Listed to keep    : ['DstpToD0Pip_D0ToKmPip']
Output top-level  : ['D0ToKmPip', 'DstpToD0Pip_D0ToKmPip']
Expected          : ['DstpToD0Pip_D0ToKmPip']

BUG: suffix dir 'D0ToKmPip' leaked : True
control 'Unrelated' dropped       : True

ROOT version

ROOT version : 6.38.04

 $ .pixi/bin/pixi exec -s root root -b -q
   ------------------------------------------------------------------
  | Welcome to ROOT 6.38.04                        https://root.cern |
  | (c) 1995-2025, The ROOT Team; conception: R. Brun, F. Rademakers |
  | Built for linuxx8664gcc on Apr 08 2026, 08:58:32                 |
  | From tags/6-38-04@6-38-04                                        |
  | With  std202302                                                  |
  | Try '.help'/'.?', '.demo', '.license', '.credits', '.quit'/'.q'  |
   ------------------------------------------------------------------

Installation method

conda-forge

Operating system

MacOS and alma9 linux

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions