Releases: amrshadid/go-dicom
Release list
v1.5.0
Fixed after the first tag
Thirteen fixes landed after v1.5.0 was first tagged, and the tag was moved to include
them. All thirteen were found by using the software rather than by testing it: three came
out of building a demonstration recording of the CLI, one out of auditing the CLI's
own help against the commands it advertises, and one out of rendering that help as a
cover image for the recording.
-
A group length is no longer reported as an unknown tag. Every DICOM group may
carry a group length element,(gggg,0000), and the dictionary enumerates only the
two with names of their own — so validating an ordinary file warned once per group:level=WARN msg="dataset: semantic validation" tag=(0008,0000) err="unknown standard tag: (0008,0000)"Six lines for a file with nothing wrong with it. Found by storing a file from the
test corpus rather than one generated here: the fixtures this project generates carry
no group lengths, which is why twelve earlier rounds of hunting log noise missed it. -
The version is reported consistently however the binary was built. The source
declares1.5.0; the release workflow stamps the git tag, which isv1.5.0. So a
released binary saidgo-dicom version v1.5.0while the same source built with
makesaid1.5.0, and the Implementation Version Name sent to peers,
GO-DICOM-1.5.0, agreed with neither. The test that exists to stop those drifting
apart could not catch it: it runs against the source default and never sees the value
the linker stamps in. A leadingvis now stripped at the point of display, so every
stamping form reads the same. -
Flags are honored wherever they are written. Go's flag package stops at the
first positional argument, so the forms the help itself documents parsed no flags at
all:$ go-dicom convert patient.dcm data.csv --format csv # wrote JSON, exit 0 $ go-dicom codify patient.dcm --output create.go # wrote to stdout, exit 0--format json,csvandniftiproduced byte-identical files. Both commands now
accept flags in any position, which is what the help already promised. -
tag-doc <tag>documents that tag. The positional argument was read and
discarded, sotag-doc 0010,0010printed the first fifty dictionary entries and
exited zero.(0010,0010),0010,0010and00100010are all accepted; junk is
refused with a message saying what a tag looks like. -
tag-docwith no arguments lists the dictionary, and writes no file. It
announced "Displaying first 50 tags", listed none, printed three hardcoded counts,
and createddicom_tags_text.txtin the working directory as a side effect — one of
which reached this repository. It now lists real entries sorted by tag, honors
-retiredand-private, counts what it actually has, and writes to a file only
when-outputnames one. -
codifyproduces Go that compiles. Two defects: it emittedpackage mainwith
nomainfunction, so the default output of a command whose purpose is runnable Go
would not build; and it decided whether a value was printable by ranging the string,
which yields U+FFFD for invalid UTF-8 — so binary values were written into the source
raw and the file failed to parse with "illegal UTF-8 encoding". Text VRs now go
throughstrconv.Quote; numeric and binary VRs are emitted as the bytes they are,
so the generated data set matches the file instead of carrying a placeholder. -
A file that is not DICOM is reported as such.
show,info,convertand
codifyaccepted any file at all:show /etc/hostsprinted a header, a column
heading, no rows, and exited zero, andshow /dev/nulldid the same without even a
warning. A file from which no element can be read is now an error. -
The CLI is called
go-dicom, everywhere. It shipped under two names:go installbuilds it asgo-dicom, after the last element of the module path, while
the Makefile and the release assets called itdicom. The help text hardcoded
go-dicom, so anyone who installed from a release was given instructions for a
command they did not have —$ dicom USAGE: go-dicom <command> [options] [arguments] $ go-dicom zsh: command not found: go-dicomThe build, the release assets and the installer now all produce
go-dicom, and the
help takes the name from how the binary was actually invoked, so it stays correct
for a renamed copy too. The release assets are renamed accordingly:
go-dicom-macos-arm64and so on. -
go-dicom helplists every command. It printed seven — the file commands —
while the bare binary printed all sixteen grouped by category, because
displayMainHelpkept a third copy of the list with its own descriptions. So the
nine network commands, which are most of what the tool does, were absent from the
listing a user reaches by typinghelp. Both listings now come from one place and
print identically. Found by renderingdicom helpas a cover image and counting
the commands on it. -
help <command>works for every command. The top-level list advertised
sixteen commands andhelp <name>knew seven of them: each of the nine network
commands reportedunknown command 'storescu', whilestorescu -hprinted a
full page of help and the command ran correctly. A user following the CLI's own
closing instruction — "Use 'go-dicom help ' for more information on a
specific command" — was told the command did not exist. Help for a command with
no hand-written page now comes from the command itself, so the two cannot drift
apart again. -
A bare TCP connect is no longer logged as an error. Opening a connection and
closing it without sending a PDU — which is what every health check,
load-balancer probe and port scan does — arrived at the association read as an
EOF and was reported at error level. A server behind a load balancer produced a
steady stream of errors describing itself working correctly. -
Ordinary presentation context negotiation is no longer reported as a problem.
Each refused context was logged at warning level, and a requestor proposing the
default set has around twenty while any server supports a subset — so every
association produced a handful of warnings, burying the refusals that matter.
An individual refusal is now debug; refusing every context, which leaves the
association established and useless, stays an error. Found by running the CLI for
a demonstration, where normal traffic looked like a fault. -
An association ending is no longer reported as an error. A read timeout and
an EOF were both logged at error level, and both are how associations ordinarily
finish: a requestor that has done its work and gone leaves the server reading an
idle connection until the network timeout fires, and a requestor that exits
without releasing gives the server an EOF — which plenty of tools do. So every
completed query produced an error describing healthy traffic. A read that fails
for any other reason is still an error. Found the same way, in the same
demonstration.
Added
-
An installer.
install.shworks out the platform, downloads the matching build
with the release'sSHA256SUMS, and refuses to install unless the checksum matches.
It prefers a directory already onPATHso nothing needs sudo, and clears the macOS
quarantine flag that otherwise stops Gatekeeper running a downloaded binary.curl -fsSL https://raw.githubusercontent.com/amrshadid/go-dicom/main/install.sh | sh
Documentation
-
The README now opens with a recording of the CLI: reading a DICOM file, standing
up an archive, storing two studies into it and querying them back. Nothing in it
is staged — every command runs against a real archive that is empty when the
recording starts. -
The README now says how to install the CLI: the script, a manual download with a
platform table and the verification step, and building from source. It previously
covered onlygo getof the library. -
The README's command list covered eleven of the sixteen CLI commands.
codify,
echoscp,getscu,qrscpandtag-docwere absent, so the only way to learn
they existed was to run the binary with no arguments.
The release as first tagged
This release started as a pass over the documentation, checking each claim against
the code. Ten defects came out of it, and the pattern in almost every one is the
one 1.4.0 was about: prose and tests agreeing with each other while the code did
something else. A data set that sent correctly over the network wrote to disk
empty. qrscp answered a query for one study with the whole archive and never
wrote a file. make build stamped a version three releases old. A multi-scan
JPEG-LS frame from any encoder could not be decoded.
One was a security defect: storescp, getscu and qrscp each wrote received
instances to a path taken from a peer-supplied SOP Instance UID without validating
it, so an unauthenticated peer could write a file anywhere the server process
could.
The features are the ones that let the library be used as an archive rather than
as a client: an on-disk instance store with a queryable index, a JPEG-LS encoder
so RLE is no longer the only syntax it can compress to, DIMSE operations that
pipeline on one association, and the tier-2 SOP classes verified against a peer
instead of only against themselves.
Where something is not done, it says so and says why — the SCP side of
asynchronous operations, JPEG 2000 encoding and decoding, sequence matching outside
the standard's own matching keys.
Added
- DIMSE operations pipeline on one association, bounded by the negotiated
window. The read side used to assume the next message belonged to the request
just sent, which is true only when one operation runs at a time — so operations
...
v1.4.0
Correctness release. Everything below was found by running this library against
pydicom's test corpus or a live peer — not by its own tests, which passed
throughout.
Fixed
-
Four N-DIMSE requests named their target with the wrong element. N-GET,
N-SET, N-ACTION and N-DELETE identify what they act on with Requested SOP
Instance UID(0000,1001); this library sent Affected SOP Instance UID
(0000,1000)— the element for messages that create or report on an instance
— and(0000,1001)did not exist in the codebase at all. Its own SCP read
back the same wrong tag, so every test passed while no other implementation
could see a target: pynetdicom answered a well-formed N-ACTION with "Received
unexpected N-ACTION service message" and aborted the association. Found by
running against pynetdicom, which now happens in CI. -
Multi-frame images reported a single frame.
NumberOfFramesis IS, and
PS3.5 §6.2 pads a value to an even length with a trailing space, so"2 "
failedstrconv.Atoiand the count silently fell back to its default of 1.
For compressed data that meant every frame after the first was discarded with
no error at all. Text values now have their padding stripped, as the standard
says they should. -
The deflated transfer syntax UID was wrong in
compress, recorded as
1.2.840.10008.1.2.4.1— a UID in the JPEG arc that is not a transfer syntax.
Real deflated files fell through to "unknown transfer syntax". -
The RLE encoder produced frames nothing could read. It emitted no segment
header, and this package's decompressor accepted that because it had the
matching defect. Its PackBits runs were also capped out of range: a replicate
run at 256 emitted a control byte meaning a two-byte literal, and a literal
run at 129 emitted the no-op marker. -
Explicit VR Big Endian files could not be read, and files written as big
endian could not be read by anything else. Three separate defects:- The short-form value length was assembled by hand as little endian at two
sites infilereader, ignoring the byte order the reader had already been
given.MR_small_bigendian.dcmparsed to 1 element; pydicom reads 72. - Values themselves stayed in big endian once the lengths were fixed, so
BitsAllocated of 16 read back as 4096 — the same bits reversed. Values are
now normalised to little endian as they are parsed, soDatasetneeds no
byte-order concept and everything downstream can assume one order. - The write side had no inverse. A file round-tripped through the library
declared big endian while holding little endian values, andfilewriter
wrote the file meta header in the data set's byte order although PS3.10
§7.1 requires it always be Explicit VR Little Endian — producing a header
whose first tag read back as(0200,0000).
Verified:
MR_small_bigendian.dcmnow parses to 72 elements and decodes to
pixels identical to pydicom's; a file written by this library and re-read in
pydicom matches on both element values and pixel data. - The short-form value length was assembled by hand as little endian at two
-
C-CANCEL aborted the association.
SCU.Cancelsent a well-formed
C-CANCEL-RQ and the SCP had no case for it, so it fell to the default branch,
which aborts. Cancelling did not merely fail — it tore down the connection and
discarded results the requestor had already received. It is now dispatched and
the association survives. -
The
fileinput to the coverage upload was silently ignored in CI, and
govulncheckwas installed from@latest, which broke the build when the
tool raised its own Go requirement. -
Text was returned as stored bytes, so most of the world's names came back as
mojibake. Decoding was reachable only throughDecodePersonNameand
DecodeTextValue, so a caller who did not know to ask got Greek, Hebrew,
Japanese or plain accented Latin as raw bytes. Two of pydicom's seventeen
charset fixtures matched its reading; all seventeen do now. Values are decoded
to UTF-8 on read and Specific Character Set is rewritten toISO_IR 192so the
data set stays self-consistent, item-scoped character sets included. -
A data set with no file meta header read as empty, with no error. PS3.5
§10.1 makes Implicit VR Little Endian the default, and that was assumed. An
explicit VR stream read that way takes the VR characters as part of the length
—(0008,0005) CS 10becomes a length of 676675 — which runs past the end of
the file, so the element is dropped and every one after it with it. pydicom
reads 24 elements from each of its two headerless fixtures; this read none. The
encoding is now taken from the first element. -
A truncated sequence item discarded the whole sequence. The complete items
before it were thrown away to punish a defect they had no part in;
DICOMDIR-nooffsetlost 51 good records because its 52nd is cut short. -
De-identification covered 38 of the 655 attributes PS3.15 names. Measured
across pydicom's corpus, 60 identifying attributes kept their original values
in files reported as de-identified — Patient's Sex in 49 of 69 files, Age in
25, Weight in 14. Sequences were never descended into, attributes named by a
range of groups (curve and overlay data) could not be looked up at all, and the
UID action silently did nothing when it met a sequence, leaving every
Referenced SOP Instance UID intact in 18 files — each one a link from the
de-identified object back to its original. Zero remain. -
filesetsearched nothing and counted wrongly.FindByModality,
FindByPatient,FindByStudyInstanceUIDandFindBySeriesInstanceUIDignored
their argument and returned every record;GetStatisticsreported the file
count as the patient, study and series counts;ScanDirectoryandAddFile
never parsed the files they listed, so nothing above them had anything to work
with; andGenerateDICOMDIRreturned an empty data set. -
The Huffman table builder dropped a bit at every empty code length.
Canonical codes lengthen at each length, including one no code has. Skipping
that shift left every longer code a bit short, so the table decoded a different
symbol than the encoder wrote. It needs an interior gap to show and no lossless
fixture had one; JPEG Lossless was wrong on any stream whose tables skip a
length. -
Byte order was not swapped for the 64-bit value representations, so a big
endian file carrying an SV, UV or OV kept its values in the wrong order and
said nothing about it. -
NIfTI export produced a file no reader could open. The header was 348 zero
bytes with the magic string at the end, sosizeof_hdrwas 0 and nibabel
answered "Cannot work out file type" while the command reported success. The
pixel data was the stored value rather than the decoded one, so a compressed
instance contributed a codestream described as an array of samples.
Added
-
A DICOM Conformance Statement (CONFORMANCE.md), in the
structure of PS3.2: SOP classes per role, transfer syntaxes negotiated versus
those readable from a file, extended negotiation, configuration, character
sets, enforced limits, and a plainly stated list of limitations. Every UID and
exported symbol it names was checked against the code.One thing it makes explicit that was easy to miss: the SCP negotiates only
Implicit and Explicit VR Little Endian by default. A big endian, deflated or
compressed data set is read correctly from a file but is not accepted on the
wire unless the application callsSetSupportedTransferSyntaxes. -
Storage Commitment (PS3.4 Annex J), as both SCU and SCP. An SCU asks a
peer to take permanent responsibility for instances it has already sent, so it
can delete its own copies:resp, err := scu.RequestStorageCommitment(ctx, &network.StorageCommitmentRequest{ TransactionUID: network.GenerateUID(), Instances: refs, }) result, err := scu.ReceiveStorageCommitmentResult(ctx)An SCP provides it by implementing
StorageCommitmentProvider, or with the
StorageCommitmentHandlerconvenience type. A handler that does not implement
it causes requests to be refused rather than silently accepted — accepting
tells the requestor it may delete its only copy.The event type is derived from the result rather than passed in, so a report
cannot claim everything succeeded while listing failures.Verified against pynetdicom, which reads the transaction UID, action type,
and every instance reference. That check runs in CI. -
commitscuCLI command, andnetwork.GenerateUID()for minting UIDs
under the UUID-derived arc (2.25, ITU-T X.667), which needs no registered
root. -
Compressed frames can be extracted.
filereaderdiscarded the Basic
Offset Table and item headers of encapsulated Pixel Data and concatenated the
fragment payloads, on the reasoning that frame boundaries would be recovered
later. They could not be — theencapspackage recovers them by parsing that
structure, soExtractEncapsulatedFramesfailed with "failed to parse basic
offset table" on every compressed file, and multi-frame images could not be
split at all.PixelDatanow holds the encapsulation exactly as it appears in
the file, matching what pydicom exposes.Verified against pydicom:
MR_small_RLE.dcmgives one 6108-byte fragment
from 6128 bytes of pixel data, andSC_rgb_rle_2frame.dcmsplits into two
664-byte fragments with a two-entry offset table — identical in both cases.This does not decode anything:
PixelArray()still cannot decompress. It is
the step that had to come first. -
RLE Lossless pixel data decodes.
Dataset.PixelArray()now decompresses
encapsulated pixel data instead of handing it to the sample parsers as though
it were raw, which failed on every compressed file with "insufficient pi...
v1.3.0
Upgrade from 1.2.0 without delay. The
ItemTagconstant in 1.2.0 was wrong, so the
nested sequence parsing introduced in that release failed on every real DICOM file
containing a sequence. 1.2.0 should be skipped.
Added
- C-GET sub-operations — a C-GET previously invoked the handler and returned a status
but transferred nothing. The SCP now sends matching instances back as C-STORE
sub-operations over the same association (PS3.4 Annex C.4.3), each on the presentation
context negotiated for its own SOP Class, with pending responses carrying the
remaining/completed/failed/warning counts. The SCU dispatches incoming C-STORE-RQ
messages toSCUConfig.OnCStoreand acknowledges each one.CGetResponse.Instancessupplies the instances to transferSCUConfig.OnCStorereceives them on the requesting sideqrscpretains received datasets and serves them, making it a working
in-memory query/retrieve server
- C-MOVE sub-operations — the SCP now opens an association to the move destination and
sends the matching instances there as C-STORE sub-operations (PS3.4 Annex C.4.2),
reporting progress back to the requestor. This completes query/retrieve: C-FIND,
C-GET, and C-MOVE all work as both SCU and SCP.CMoveResponse.Instancessupplies the instances to moveQueryRetrieveHandler.OnMoveInstancesreturns them from a handler; the older
OnMove, which could not transfer anything, is deprecated but still honoredSCPConfig.MoveDestinationsandSCPConfig.ResolveMoveDestinationresolve a
destination AE title to an address; an unresolvable title is answered with
StatusMoveDestUnknownqrscp -move-dest AETITLE=host:portconfigures destinations from the CLI
- Sequence writing in
filewriter—DataElement.Itemsholds nested
SequenceItemvalues, closing the read → write → read round trip. Items are written
with explicit lengths and implicit-style item headers as PS3.5 Section 7.5 requires. - Raw DICOM data sets without a file meta header —
ReadDICOMFilerequired the
128-byte preamble and DICM prefix, so a raw stream, which is what modalities produce and
what travels on the network, could not be read despite the README listing it as
supported. The reader now detects which form the stream is and falls back to implicit VR
little endian per PS3.5 Section 10.1, recording the outcome inDICOMFile.HasPreamble. DICOMFile.MetaElements— the group-0002 elements as they appeared in the file,
for callers that need to display the header verbatim.- Interoperability testing against pynetdicom and dcmtk —
scripts/interop-test.sh
plus a CI job. Exercises C-ECHO and C-STORE in both directions and C-GET
sub-operations, using pydicom'sCT_small.dcmas the fixture and pydicom as the
verifier rather than this library's own reader. Fails when no third-party peer is
available, so a broken install cannot pass by skipping.
Fixed
- The CLI parsed files with a second, broken parser —
show,info,convert, and
codifyused a parser incli/helpers.goseparate fromfilereader, which received
none of this cycle's fixes. It read the file in 64 KiB chunks and parsed each
independently, so an element straddling a boundary desynchronized the stream: on a
268 KB file where pydicom reports 258 elements it printed roughly 38 and ended with an
invented element whose VR was two arbitrary bytes. It also never descended into
sequences, and classified SQ and UT as short-form VRs. The CLI now usesfilereader,
reports 269 elements for the same file, and indents sequence contents by nesting depth. ItemTagwas0xFFFE0000, not0xFFFEE000(critical) — the constant was missing
a digit and decoded as (FFFE,0000). Sequence parsing, added in 1.2.0, therefore failed
on every real DICOM file containing a sequence, rejecting the correct item tag as
unexpected. The unit tests passed because they built their fixtures with the same wrong
constant.tag.FromBytesandtag.ToBytestransposed group and element — both treated the
4-byte tag as a single uint32 rather than two consecutive 16-bit values. Undetected
because the only test case was PatientName (0010,0010), where group equals element, and
because the two functions were each other's inverse.- Sequences were lost over the network —
EncodeDatasetskipped sequence elements,
andstorescubuilt its dataset fromelem.Value, which is nil for a sequence. An
instance sent to a peer arrived with its sequence present but empty. - An empty data set sent no PDU at all —
SendPDatalooped over the payload, so a
zero-length data set produced nothing after the command had already announced one,
leaving the peer blocked until the DIMSE timeout. Reachable with any keyless C-FIND or
C-GET identifier. QueryRetrieveHandler.OnGetresults are now transferred rather than only counted.
Install
go install github.com/amrshadid/go-dicom@v1.3.0Or download a binary below and place it on your PATH:
| Platform | File |
|---|---|
| Linux x86-64 | dicom-linux-amd64 |
| Linux ARM64 | dicom-linux-arm64 |
| macOS Intel | dicom-macos-amd64 |
| macOS Apple Silicon | dicom-macos-arm64 |
| Windows x86-64 | dicom-windows-amd64.exe |
Checksums are in SHA256SUMS:
sha256sum -c SHA256SUMSv1.2.0
This release makes go-dicom interoperable with conforming DICOM implementations. Two defects meant data written or sent by the library could not be read by any other DICOM software — neither was caught by the existing tests, because each side of the library was self-consistent in its own wrongness.
Upgrade notes
Files written by v1.1.1 and earlier have malformed meta headers. They will now read back with an empty MediaStorageSOPClassUID / MediaStorageSOPInstanceUID and a wrong TransferSyntaxUID, because the reader now looks at the correct tags. Files written by v1.2.0 are correct. Any archive produced by an earlier version should be rewritten.
Warnings moved from stdout to stderr. dataset.Add, the file reader, and the file writer's validation path used fmt.Printf, which corrupted piped output with no way to silence it. These now go through config.Logger. If you scrape stdout for Warning: lines, update accordingly — use config.SetLogger to redirect.
No breaking API changes.
Interoperability
Written files were not valid DICOM
WriteFileMetaInfo used the wrong group-0002 tags throughout: the SOP Class UID went to (0002,0010) — which is Transfer Syntax UID — the SOP Instance UID to (0002,0012) — Implementation Class UID — and so on. Reading a written file back reported the SOP Class UID as the transfer syntax and left both SOP UIDs empty.
BEFORE AFTER
SOPClassUID = "" SOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
SOPInstanceUID = "" SOPInstanceUID = "1.2.3.4.5.6.7.8.9.100"
TransferSyntax = "1.2.840.10008.5.1.4.1.1.2" TransferSyntax = "1.2.840.10008.1.2.1"
Tags now follow PS3.6. The Type 1 (0002,0001) File Meta Information Version is always written. The reader was also looking for AE titles at (0002,0100..0102), a range belonging to Private Information attributes.
Data sets ignored the negotiated transfer syntax
Every DIMSE data set was encoded as Implicit VR Little Endian regardless of what was negotiated. Since Explicit VR Little Endian is proposed first by default, essentially every real peer received an unparsable data set. Adds a dataset codec covering Implicit VR LE, Explicit VR LE, Explicit VR BE, and Deflated Explicit VR LE, threaded through every DIMSE path.
Odd-length values corrupted the byte stream
DICOM requires even-length values (PS3.5 §7.1.1); one odd value misaligns everything after it. Padding is now applied centrally using the VR's designated character, rather than left to callers.
Security
Four vectors reachable from an unauthenticated peer:
- SCP process crash — a presentation context with zero transfer syntax sub-items caused an index-out-of-range panic in
NegotiatePresentationContexts, terminating the whole server. - ~4 GiB allocation from a 6-byte header —
DecodePDUsized its buffer from the peer-controlled 32-bit PDU length before reading. Now capped atMaxPDULengthLimit(128 MiB). - Oversized PDV allocation —
decodeDataTFallocated from the PDV length, which is independent of the enclosing PDU length. - Multi-gigabyte allocation from a file — the reader allocated directly from the declared Value Length; an element claiming
0xFFFFFFFFallocated that much. Lengths above 16 MiB are now verified against the bytes remaining.
Also hardened: DecodeCommandDataset used Read rather than io.ReadFull (silent zero-padded values on short reads), and the role-selection and user-identity decoders trusted peer-supplied lengths without bounds checks.
Features
Nested sequence (SQ) parsing
The reader stopped at the first Item or Sequence Delimitation Item, so everything from the first sequence onward was silently dropped — any Structured Report, multi-frame functional group, or referenced-image sequence was read only up to that point.
Now parses recursively: defined- and undefined-length sequences and items, sequences under implicit VR (VR recovered from the dictionary), empty sequences, and encapsulated fragmented pixel data. Nesting is bounded by MaxSequenceDepth (64).
DICOMFile.GetDataset()
Materialises the parsed tree as a Dataset with nested sequences as child Datasets. The README documented this method but it did not exist — the documented example did not compile.
Extended negotiation, actually negotiated
UserInformationItem neither emitted nor parsed the extended sub-items, so async operations, SCP/SCU role selection, and user identity were never negotiated despite being listed as supported. Now wired through SCUConfig.ExtendedNegotiation, with Association.PeerUserInformation() and RoleSelectionFor() exposing the outcome.
Other fixes
SCPConfig.MaxAssociationswas documented but never read — the server accepted unbounded concurrent associations. Now enforced, with an A-ASSOCIATE-RJ (local-limit-exceeded) rather than a dropped socket.QueryRetrieveHandler.OnGethad noHandleCGetmethod, so setting it silently did nothing.SCU.NEventReportreported the wrongMessageIDRespondedToand burned an extra message ID.
Known limitations
Now stated plainly in the README rather than implied to work:
- C-MOVE / C-GET as an SCP send no C-STORE sub-operations. Both work fully as an SCU. This is the largest remaining gap.
- Asynchronous operations are negotiated but not enforced — the SCU is serial.
- No transfer syntax transcoding.
filewriterdoes not yet serialiseSQelements.show/info/convertuse a separate flat parser that does not descend into sequences.
Verification
Every commit builds and passes independently, so the history is bisectable. gofmt, go vet, and golangci-lint clean; 29/29 packages passing under -race on Linux, macOS, and Windows.
End-to-end verified against real TCP with the built CLI: storescp receiving from echoscu and storescu, with the received file re-read and every value compared.
New coverage: per-vector security regression tests, transfer-syntax round trips, sequence parsing, file meta tag assignments, and a full write → C-STORE → receive → read → compare integration test.
Full changelog: v1.1.1...v1.2.0
v1.1.1
go-dicom v1.1.1
See CHANGELOG.md for details.
Download Binaries
| Platform | Download |
|---|---|
| Linux (x86_64) | dicom-linux-amd64 |
| Linux (ARM64) | dicom-linux-arm64 |
| macOS (Intel) | dicom-macos-amd64 |
| macOS (Apple Silicon) | dicom-macos-arm64 |
| Windows (x86_64) | dicom-windows-amd64.exe |
Installation
Using Go
go install github.com/amrshadid/go-dicom@v1.1.1Download Binary
Download the appropriate binary for your platform above and add it to your PATH.
Verification
Verify the integrity of downloaded binaries using the SHA256SUMS file:
sha256sum -c SHA256SUMSSee CHANGELOG.md for complete release notes.
v1.1.0
go-dicom v1.1.0
See CHANGELOG.md for details.
Download Binaries
| Platform | Download |
|---|---|
| Linux (x86_64) | dicom-linux-amd64 |
| Linux (ARM64) | dicom-linux-arm64 |
| macOS (Intel) | dicom-macos-amd64 |
| macOS (Apple Silicon) | dicom-macos-arm64 |
| Windows (x86_64) | dicom-windows-amd64.exe |
Installation
Using Go
go install github.com/amrshadid/go-dicom@v1.1.0Download Binary
Download the appropriate binary for your platform above and add it to your PATH.
Verification
Verify the integrity of downloaded binaries using the SHA256SUMS file:
sha256sum -c SHA256SUMSSee CHANGELOG.md for complete release notes.
v1.0.0
go-dicom v1.0.0
See CHANGELOG.md for details.
Download Binaries
| Platform | Download |
|---|---|
| Linux (x86_64) | dicom-linux-amd64 |
| Linux (ARM64) | dicom-linux-arm64 |
| macOS (Intel) | dicom-macos-amd64 |
| macOS (Apple Silicon) | dicom-macos-arm64 |
| Windows (x86_64) | dicom-windows-amd64.exe |
Installation
Using Go
go install github.com/amrshadid/go-dicom@v1.0.0Download Binary
Download the appropriate binary for your platform above and add it to your PATH.
Verification
Verify the integrity of downloaded binaries using the SHA256SUMS file:
sha256sum -c SHA256SUMSSee CHANGELOG.md for complete release notes.