Summary
parseDicom fails with dicomParser:parseDicomDataSetExplicit: buffer overrun on valid explicit-VR files that contain an Extended Offset Table (7FE0,0001, VR OV). The parser's list of "long form" VRs predates the three VRs added to the standard in 2019 (OV, SV, UV — DICOM PS3.5 Table 7.1-1), so it reads their 12-byte header as the 8-byte short form and derails.
Root cause
getDataLengthSizeInBytesForVR in src/readDicomElementExplicit.js:
var getDataLengthSizeInBytesForVR = function (vr) {
if (vr === 'OB' || vr === 'OD' || vr === 'OL' || vr === 'OW' || vr === 'SQ' ||
vr === 'OF' || vr === 'UC' || vr === 'UR' || vr === 'UT' || vr === 'UN') {
return 4;
}
return 2;
};
OV, SV, and UV all use the 12-byte explicit-VR header (2-byte VR + 2-byte reserved + 4-byte length), but this list doesn't include them, so the parser reads the two reserved bytes (0x0000) as a 2-byte length. The element gets length 0, the stream position lands in the middle of the real header, and the following bytes are parsed as garbage tags/lengths — typically ending in a buffer overrun throw, or (worse) in silently wrong elements when the garbage happens to stay in bounds.
Real-world impact
Whole-slide imaging files from TCIA's CMB-MML collection carry Extended Offset Tables. In one five-file study, four files have an EOT and all four fail to parse with dicom-parser 1.8.21; the one file without an EOT parses fine.
Downstream, in a converter using a chunked byte source (Static-DICOMWeb's mkdicomweb — found while verifying RadicalImaging/Static-DICOMWeb#129 against this study), a 5 GB / 394,830-frame pyramid failed silently: instance metadata was emitted but zero frames were extracted, with no error reported. Removing the 22-byte EOT element from the same file made it convert perfectly, confirming the diagnosis.
EOTs are increasingly common in large multi-frame objects (WSI, video), so this will surface more often over time.
Minimal reproduction
import dicomParser from 'dicom-parser'; // 1.8.21
function buildFile(vr, valueBytes) {
const chunks = [Buffer.alloc(128), Buffer.from('DICM')];
const ts = Buffer.from('1.2.840.10008.1.2.1\0'); // explicit VR little endian
const tsEl = Buffer.concat([Buffer.from([0x02,0x00,0x10,0x00]), Buffer.from('UI'),
Buffer.from([ts.length & 0xff, ts.length >> 8]), ts]);
const gl = Buffer.concat([Buffer.from([0x02,0x00,0x00,0x00]), Buffer.from('UL'),
Buffer.from([4,0]), Buffer.alloc(4)]);
gl.writeUInt32LE(tsEl.length, 8);
chunks.push(gl, tsEl);
// (7FE0,0001) with the given VR, long form: VR(2) + reserved(2) + length(4)
const el = Buffer.concat([Buffer.from([0xe0,0x7f,0x01,0x00]), Buffer.from(vr),
Buffer.from([0,0]), Buffer.alloc(4), valueBytes]);
el.writeUInt32LE(valueBytes.length, 8);
chunks.push(el);
return Buffer.concat(chunks);
}
for (const [label, vr, value] of [
['control: same bytes, VR=OB', 'OB', Buffer.alloc(16, 0xff)],
['OV, offsets with high bytes', 'OV', Buffer.alloc(16, 0xff)],
['OV, small offsets (zeros)', 'OV', Buffer.alloc(16, 0x00)],
]) {
try {
const ds = dicomParser.parseDicom(buildFile(vr, value));
console.log(label, '-> PARSE OK, elements:', Object.keys(ds.elements).join(' '));
} catch (e) {
console.log(label, '-> THROWS:', e.exception || e.message || e);
}
}
Output on 1.8.21:
control: same bytes, VR=OB -> PARSE OK, elements: x7fe00001 x00020000 x00020010
OV, offsets with high bytes -> THROWS: dicomParser:parseDicomDataSetExplicit: buffer overrun
OV, small offsets (zeros) -> THROWS: dicomParser.readFixedString: attempt to read past end of buffer
Byte-identical input parses correctly when the VR is OB; only the unrecognized VR handling differs.
Suggested fix
Add the three 2019 VRs to the long-form list:
if (vr === 'OB' || vr === 'OD' || vr === 'OL' || vr === 'OV' || vr === 'OW' ||
vr === 'SQ' || vr === 'OF' || vr === 'SV' || vr === 'UV' ||
vr === 'UC' || vr === 'UR' || vr === 'UT' || vr === 'UN') {
return 4;
}
Their element length field is still 32-bit, so no other header changes are needed for reading. (Typed accessors for 64-bit OV/SV/UV values would be a nice follow-up, but recognizing the header is what stops the misparse.)
Summary
parseDicomfails withdicomParser:parseDicomDataSetExplicit: buffer overrunon valid explicit-VR files that contain an Extended Offset Table (7FE0,0001, VROV). The parser's list of "long form" VRs predates the three VRs added to the standard in 2019 (OV,SV,UV— DICOM PS3.5 Table 7.1-1), so it reads their 12-byte header as the 8-byte short form and derails.Root cause
getDataLengthSizeInBytesForVRinsrc/readDicomElementExplicit.js:OV,SV, andUVall use the 12-byte explicit-VR header (2-byte VR + 2-byte reserved + 4-byte length), but this list doesn't include them, so the parser reads the two reserved bytes (0x0000) as a 2-byte length. The element gets length 0, the stream position lands in the middle of the real header, and the following bytes are parsed as garbage tags/lengths — typically ending in a buffer overrun throw, or (worse) in silently wrong elements when the garbage happens to stay in bounds.Real-world impact
Whole-slide imaging files from TCIA's CMB-MML collection carry Extended Offset Tables. In one five-file study, four files have an EOT and all four fail to parse with dicom-parser 1.8.21; the one file without an EOT parses fine.
Downstream, in a converter using a chunked byte source (Static-DICOMWeb's
mkdicomweb— found while verifying RadicalImaging/Static-DICOMWeb#129 against this study), a 5 GB / 394,830-frame pyramid failed silently: instance metadata was emitted but zero frames were extracted, with no error reported. Removing the 22-byte EOT element from the same file made it convert perfectly, confirming the diagnosis.EOTs are increasingly common in large multi-frame objects (WSI, video), so this will surface more often over time.
Minimal reproduction
Output on 1.8.21:
Byte-identical input parses correctly when the VR is
OB; only the unrecognized VR handling differs.Suggested fix
Add the three 2019 VRs to the long-form list:
Their element length field is still 32-bit, so no other header changes are needed for reading. (Typed accessors for 64-bit
OV/SV/UVvalues would be a nice follow-up, but recognizing the header is what stops the misparse.)