Skip to content

Support 32-bit platforms, and fix the three bugs that were hiding there - #75

Merged
xbmlz merged 1 commit into
mainfrom
bump-codecs-v1.3.0
Aug 25, 2026
Merged

xbmlz merged 1 commit into
mainfrom
bump-codecs-v1.3.0

Conversation

@xbmlz

@xbmlz xbmlz commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Bumps golibjpeg and goopenjpeg to v1.3.0. Those releases add prebuilt
libraries for darwin/amd64 and windows/arm64 (six platforms each, up from
four) and — the part that matters here — they now build on every platform Go
targets rather than only the ones they ship a library for, returning an error
wrapping ErrUnsupportedPlatform instead of failing to compile.

That made "importing godicom is safe anywhere" a claim worth checking. It was
false: godicom itself did not compile for a 32-bit target. Fixing that turned up
two bugs that have nothing to do with cross-compilation.

Fixed

The data dictionary answered US for nearly every tag on 32-bit

All 88 repeater masks were built with fmt.Sscanf(mask2Str, "%x", &mask2) into
int fields, with the error discarded. A mask like 0xFF00FFFF does not fit
in a 32-bit int, so on those platforms all 88 parsed to zero — and
(tag ^ value) & 0 == 0 is true for every tag. Whichever repeater happened to
be first in map iteration order became the answer for every non-private tag:

  • LookupVR(PatientName) returned US, not PN
  • group lengths returned US, not UL
  • IsRepeaterTag was true for everything
  • the reader raised 24 spurious VR-mismatch diagnostics on ordinary files

A probe on windows/386 before the fix: 88 of 88 masks had mask == 0, and
every tag matched 50xx0105. Rebuilt with nibble arithmetic into uint32 —
there is no error to drop and no platform-dependent width to overflow. 64-bit
platforms were unaffected.

ParseTag rejected every hex tag with a group ≥ 0x8000 — on all platforms

strconv.ParseInt(v, 16, 32) rejects anything from 0x80000000 up, so:

ParseTag("FFFEE000")  // godicom: unknown tag keyword "FFFEE000"

Those are the item, item-delimiter and sequence-delimiter tags that godicom's own
sequence and encapsulation code is built from, and the ones a JSONKey
round-trip is most likely to hit. pydicom's tag.py uses int(arg, 16), which
has no width at all, so ParseUint is the correct port. The parenthesised form
(FFFE,E000) always worked — it parses two 16-bit halves.

Confirmed the new test catches it by reverting to ParseInt: 5 subtests fail.

encaps.Encapsulate did not compile on 32-bit

The Basic Offset Table guard read total > (1<<32)-1. That is exact in pydicom
(encaps.py:1124) because Python integers are arbitrary-precision; in Go the
untyped constant overflows int at compile time on a 32-bit target.

Changed

The compile failures were all one mistake: a declared value length is unsigned
32 bits
, and elementHeader.Length was an int. 0xFFFFFFFF — undefined
length — is -1 in an int on a 32-bit platform, so casting to make it build
would have shipped a binary that silently misread every undefined-length
sequence and every encapsulated Pixel Data element. A merely-compiling build
would have been worse than the compile error.

So uint32 is carried through the header, deferred-read and sequence paths
instead. No exported signature changes, and it removed two existing
uint32(length) casts at call sites.

Added

  • CI cross-build job: vets windows/386, linux/386, linux/arm
    (incl. GOARM=5), linux/riscv64, linux/ppc64le, js/wasm, wasip1/wasm,
    and runs the full test suite on linux/386. go vet rather than
    go build so _test.go files are type-checked too. The test run is the point:
    the mask bug compiled fine and only failed when executed. linux/mips and
    linux/mipsle are excluded — purego does not build for them
    (func.go:184:9: undefined: addStruct), which is upstream.
  • skipWithoutNativeCodec in both godicom and pixels test packages, so the
    15 tests that genuinely need a native codec skip rather than fail where no
    library exists. It cannot mask a regression on a supported platform: on 64-bit
    exactly 1 test skips (a pre-existing fixture skip), so all 14 guarded tests
    still run and assert there.

Docs

  • README gains a Platforms section and per-platform binary sizes. Each binary
    embeds one platform's libraries, never all twelve: cmd/godicom on
    linux/amd64 is 11.0 MB, where embedding every library unconditionally would
    make it 24.3 MB. All numbers measured, not estimated.
  • CompressPixelData's doc comment listed its supported targets without HTJ2K,
    which pixels.EncodeFrame has dispatched on for some time.

Verification

Result
go test ./... (amd64) pass, 1 skip
GOARCH=386 go test ./... (real run via WoW64) pass, 15 skips
go vet ./... × 8 cross targets clean
staticcheck -checks=all clean
gofmt -l clean

🤖 Generated with Claude Code

Bumps golibjpeg and goopenjpeg to v1.3.0, which add prebuilt libraries for
darwin/amd64 and windows/arm64 and, more importantly, now build on every
platform Go targets instead of only the ones they ship a library for. That made
"godicom is importable anywhere" a claim worth checking, and it was false:
godicom itself did not compile for a 32-bit target. Making it compile turned up
two bugs that had nothing to do with cross-compilation.

The dictionary one is the serious one. All 88 repeater masks were parsed with
fmt.Sscanf into int fields with the error discarded. A mask like 0xFF00FFFF does
not fit in a 32-bit int, so on those platforms every mask was zero, and
(tag ^ value) & 0 == 0 is true for every tag: whichever repeater came first in
map order answered for every non-private tag. LookupVR returned US for
PatientName and for group lengths, and IsRepeaterTag was true for everything.
Rebuilt with nibble arithmetic into uint32 -- no error to drop, no width to
overflow.

ParseTag was broken on every platform, not just 32-bit: strconv.ParseInt with a
bitSize of 32 rejects anything from 0x80000000 up, so ParseTag("FFFEE000")
failed with "unknown tag keyword". Those are the item and delimiter tags the
sequence and encapsulation code is built from. pydicom's int(arg, 16) has no
width, so ParseUint is the port.

The compile failures were all one mistake: a declared value length is unsigned
32 bits, and elementHeader.Length was an int. 0xFFFFFFFF -- undefined length --
is -1 in an int on a 32-bit platform, so simply casting to make it build would
have produced a binary that misread every undefined-length sequence. Carrying
uint32 through the header, deferred-read and sequence paths instead removed two
existing uint32() casts at call sites.

CI now vets eight cross targets and runs the full suite on linux/386, because
the mask bug compiled fine and only failed when run. mips and mipsle are left
out: purego does not build for them.

Tests that need a native codec skip instead of failing where no library exists,
and the README documents platform support and the per-platform binary cost --
one platform's libraries per binary, 11.0 MB on linux/amd64 against 24.3 MB if
all twelve were embedded unconditionally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xbmlz
xbmlz merged commit b2f411d into main Aug 25, 2026
6 checks passed
@xbmlz
xbmlz deleted the bump-codecs-v1.3.0 branch August 25, 2026 03:00
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.

1 participant