You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suspicious TH1D object with fByteCount == 0 (e.g. uproot-issue-250.root) #16
Findings below come from the root-io-spec reference now vendored in this repo at reference/root-io-spec (pinned at 9c3f776). It answers this issue directly, and the answer is that nothing is suspicious about these records — they are ROOT 4 histograms and our framing reader is wrong.
And on a file older than ROOT 5, an ordinary class's payload may have no byte count either. […] Measured on the two ROOT 4.00/00 files of the foreign corpus: 19 records across them — TH1D, TH2D — begin 00 01 00 03 40 00 ..., a version word, a version word, and only then a byte count.
Those "two ROOT 4.00/00 files" are exactly our two xfails. I confirmed the bytes: uproot-issue-250.root header fVersion is 40000, and the TH1D record Eabs at offset 35698 decompresses to
uproot-from-geant4.root is also fVersion 40000 (10 TH1D + 5 TH2D records; 15 + 4 = the spec's 19).
The decision rule is structural, not a file-version test — Buffer framing §3: read a u32; if kByteCountMask is clear, rewind 4 bytes and read an i16 version with "no byte count".
What that means here
StreamHeader.read (src/rootfilespec/bootstrap/streamedobject.py:51) implements only the object-slot rule (Buffer §6), where a word without kByteCountMask is a back-reference. A version word needs the different rule above. So:
_read_all_members (src/rootfilespec/bootstrap/streamedobject.py:223) needs a version-word reader with an optional byte count, at every nesting level (TH1's 00 03 is bare too);
the TH1D/TH2D guard at streamedobject.py:233-238 then goes away;
the end_position assertion at streamedobject.py:261 must be skipped when there is no byte count.
Both classes are otherwise decodable from the file's own info: TH1D v1 = TH1 + TArrayD, TH1 v3 = TNamed(67), TAttLine/TAttFill/TAttMarker(0), fNcells, three TAxis… — all of which are framed normally in the bytes. (Small note for upstream: §2.3 says "only the TNamed base inside is framed"; the TAtt* bases are framed too. The precise 00 01 00 03 40 00 statement in the same paragraph is exact.)
The other weird thing in this file
TAtt3D (src/rootfilespec/bootstrap/assumed.py:38-47): both files' TH3 v4 info lists TStreamerBase TAtt3D (fType 0) but carries no TAtt3D streamer info, violating Streamer-driven reading §10 invariant 5. Neither file contains a TH3 object, so this only bites because we materialise every class in the list eagerly. TAtt3D is ClassDef(TAtt3D,1) with no data members, so its kBase contribution is a byte count and a version word and nothing else — the current empty stub is the right shape.
Verdict: reader gap, not a bad file. ROOT reads both files without complaint. Size: small-to-medium, confined to streamedobject.py, no dependencies.
Activity
nsmith- commentedon Apr 25, 2025
Noting that
rootfilespec/src/rootfilespec/bootstrap/assumed.py
Lines 23 to 32 in 5b5b47d
the file reported in scikit-hep/uproot5#250 has two weird things going on
nsmith- commentedon Sep 18, 2026
Findings below come from the root-io-spec reference now vendored in this repo at
reference/root-io-spec(pinned at9c3f776). It answers this issue directly, and the answer is that nothing is suspicious about these records — they are ROOT 4 histograms and our framing reader is wrong.The format fact
Buffer framing §2.3 — "A record's object data does not always begin with one":
Those "two ROOT 4.00/00 files" are exactly our two xfails. I confirmed the bytes:
uproot-issue-250.rootheaderfVersionis 40000, and theTH1DrecordEabsat offset 35698 decompresses touproot-from-geant4.rootis alsofVersion40000 (10TH1D+ 5TH2Drecords; 15 + 4 = the spec's 19).The decision rule is structural, not a file-version test — Buffer framing §3: read a
u32; ifkByteCountMaskis clear, rewind 4 bytes and read ani16version with "no byte count".What that means here
StreamHeader.read(src/rootfilespec/bootstrap/streamedobject.py:51) implements only the object-slot rule (Buffer §6), where a word withoutkByteCountMaskis a back-reference. A version word needs the different rule above. So:_read_all_members(src/rootfilespec/bootstrap/streamedobject.py:223) needs a version-word reader with an optional byte count, at every nesting level (TH1's00 03is bare too);TH1D/TH2Dguard atstreamedobject.py:233-238then goes away;end_positionassertion atstreamedobject.py:261must be skipped when there is no byte count.Both classes are otherwise decodable from the file's own info:
TH1Dv1 =TH1+TArrayD,TH1v3 =TNamed(67),TAttLine/TAttFill/TAttMarker(0),fNcells, threeTAxis… — all of which are framed normally in the bytes. (Small note for upstream: §2.3 says "only theTNamedbase inside is framed"; theTAtt*bases are framed too. The precise00 01 00 03 40 00statement in the same paragraph is exact.)The other weird thing in this file
TAtt3D(src/rootfilespec/bootstrap/assumed.py:38-47): both files'TH3v4 info listsTStreamerBase TAtt3D(fType 0) but carries noTAtt3Dstreamer info, violating Streamer-driven reading §10 invariant 5. Neither file contains aTH3object, so this only bites because we materialise every class in the list eagerly.TAtt3DisClassDef(TAtt3D,1)with no data members, so itskBasecontribution is a byte count and a version word and nothing else — the current empty stub is the right shape.Verdict: reader gap, not a bad file. ROOT reads both files without complaint. Size: small-to-medium, confined to
streamedobject.py, no dependencies.