Skip to content

fix(dicom): EXPECT_FRAG crash on undefined-length UN sequences in fromXML - #431

Open
jessesaga wants to merge 1 commit into
OpenIntegrationEngine:mainfrom
SagaHealthcareIT:fix/dicom-un-sequence-expect-frag
Open

fix(dicom): EXPECT_FRAG crash on undefined-length UN sequences in fromXML#431
jessesaga wants to merge 1 commit into
OpenIntegrationEngine:mainfrom
SagaHealthcareIT:fix/dicom-un-sequence-expect-frag

Conversation

@jessesaga

Copy link
Copy Markdown

Fixes #430

Summary

DICOM channels with a filter or transformer fail with IllegalStateException: state:EXPECT_FRAG on messages that carry an undefined-length sequence under a private tag dcm4che2 resolves to VR UN (FujiFILM 0029,E131, Siemens MEDCOM 0029,1140, and similar). Root cause is an asymmetry inside dcm4che2 2.0.29's XML round trip: SAXWriter emits the wire VR (vr="UN") for such sequences, but ContentHandlerAdapter only recognises vr="SQ" as a sequence and treats everything else as fragments.

This PR fixes it reader-side in DICOMSerializer.fromXML, inside the DOM pre-pass that already exists there: an element whose <item> children contain dataset elements gets vr="SQ" before the SAX parse. 25 lines of code plus a helper.

Why this shape

  • Same decision the binary reader makes. DicomInputStream decides SQ-vs-fragments by item content, not by VR. The predicate here ("item has an <attr> child") mirrors that, so the resulting DicomObject matches what a direct binary parse produces. I checked the alternative of keying on vr="UN" len="-1" and it diverges from the binary reader on a sequence whose items are all empty, so I did not use it.
  • No change to what transformer scripts see. toXML is untouched. Fixing the writer to emit SQ would silently change @vr for existing scripts.
  • No change to emitted DICOM. The bytes coming out of the transformer path are byte-identical to the no-transformer pass-through path, for implicit and explicit VR Little Endian.
  • Strictly additive. Every XML that parses today produces the identical object; only inputs that threw before change behaviour. Encapsulated pixel data (fragment items holding only text) never trips the predicate.
  • Self-retiring. The rewrite is skipped when vr is already SQ, so it becomes a no-op if the DICOM backend ever emits SQ itself.

Testing

  • New DICOMSerializerUnSequenceTest (JUnit 4, builds its DICOM programmatically, no binary fixtures):
    • testUndefinedLengthUnSequenceRoundTrip: asserts the toXML output actually carries the broken wire shape (vr="UN" with a structured <item>), then round-trips and checks the nested element and a defined-length UN blob survive.
    • testEncapsulatedPixelDataFragmentsUntouched: regression guard for the fragment path.
  • On unpatched main, the first test fails with the exact production signature (state:EXPECT_FRAG). Patched, ./gradlew test -PdisableSigning=true is green.

Note on naming: the existing DICOMSerializerTests is not picked up by the **/*Test.class filter (and references tests/test-dicom-*.dcm fixtures that are not in the repo), so the new class follows the *Test convention the build actually runs.

…mXML

dcm4che2 2.0.29's XML round trip is asymmetric for sequences read from
implicit-VR private tags (e.g. FujiFILM 0029,E131 / Siemens MEDCOM
0029,1140). DicomInputStream treats UN + undefined length holding dataset
items as SQ in memory, but the streaming SAXWriter used by toXML emits the
wire VR (vr="UN" len="-1") with structured <item> children. On fromXML,
ContentHandlerAdapter only enters its sequence state for vr="SQ" and
otherwise expects fragments, throwing
IllegalStateException("state:EXPECT_FRAG") when the item's dataset
elements arrive. Net effect: any DICOM channel with a filter or
transformer fails on such messages.

Fix reader-side in fromXML's existing DOM walk: rewrite vr to "SQ" on
elements whose <item> children contain renamed <attr> elements. This is
the same content-based decision DicomInputStream makes for the binary
form, so the resulting DicomObject, and the DICOM bytes re-encoded from
it, are identical to the no-transformer pass-through path (verified
byte-for-byte for implicit and explicit VR LE). Fragment containers
(encapsulated pixel data, whose items hold only base64/hex text) are
untouched, toXML output is unchanged so transformer scripts see identical
XML, every previously-parsing input produces an identical DicomObject,
and the rewrite is a no-op if a future dcm4che emits SQ itself.

The test builds its DICOM programmatically (no binary fixtures), asserts
the toXML output actually carries the broken wire shape before parsing
it, and covers the encapsulated-pixel-data path the predicate must not
disturb. Pre-fix it fails with the exact production signature; post-fix
the server suite is green.

Signed-off-by: Jesse Dowell <jesse.dowell@gmail.com>
@jcdlbs
jcdlbs force-pushed the fix/dicom-un-sequence-expect-frag branch from 98ad7ed to b99c630 Compare September 2, 2026 19:09
@jessesaga
jessesaga marked this pull request as ready for review September 2, 2026 19:13
@mgaffigan
mgaffigan requested a lite review from Copilot September 2, 2026 21:57

@mgaffigan mgaffigan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks reasonable, but I'd like to hear from someone who knows DICOM before approving.

From a cleanliness perspective, I don't love that we're editing the XML after construction rather than fixing the generator, though.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped to the XML pre-pass, aligns with documented library behavior, and is covered by targeted regression tests.

Pull request overview

Fixes a dcm4che2 XML round-trip crash in DICOMSerializer.fromXML when undefined-length private-tag sequences are emitted with vr="UN" and structured <item> children, by rewriting those elements to vr="SQ" before SAX parsing (matching the binary reader’s content-based behavior).

Changes:

  • Add a DOM pre-pass in DICOMSerializer.fromXML to detect dataset-like <item> content and force vr="SQ" to avoid EXPECT_FRAG state errors.
  • Introduce a small helper to detect dataset-item shape (<item> containing <attr> children).
  • Add a new JUnit test class covering the undefined-length UN sequence round-trip and guarding against regressions for encapsulated pixel data fragments.
File summaries
File Description
server/src/main/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializer.java Rewrites VR to SQ for dataset-like <item> containers before SAX parsing to prevent EXPECT_FRAG crashes.
server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerUnSequenceTest.java Adds regression tests for the undefined-length UN sequence crash and ensures fragment containers remain untouched.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +135 to +148
if (items != null) {
for (int i = 0; i < items.getLength(); i++) {
Node itemNode = items.item(i);
Node parentNode = itemNode.getParentNode();

if (parentNode instanceof Element && hasAttrElementChild(itemNode)) {
Element parentElement = (Element) parentNode;

if (parentElement.hasAttribute("vr") && !parentElement.getAttribute("vr").equals("SQ")) {
parentElement.setAttribute("vr", "SQ");
}
}
}
}
Comment on lines +117 to +124
private String writeDicomFile(BasicDicomObject dcm, String transferSyntaxUid) throws Exception {
dcm.initFileMetaInformation(transferSyntaxUid);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DicomOutputStream dos = new DicomOutputStream(baos);
dos.writeDicomFile(dcm);
dos.close();
return Base64.encodeBase64String(baos.toByteArray());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] DICOM channels with a filter/transformer fail with IllegalStateException: state:EXPECT_FRAG on private undefined-length sequences

4 participants