Skip to content

Releases: amrshadid/go-dicom

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 23:03
28b6551

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
    declares 1.5.0; the release workflow stamps the git tag, which is v1.5.0. So a
    released binary said go-dicom version v1.5.0 while the same source built with
    make said 1.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 leading v is 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, csv and nifti produced 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, so tag-doc 0010,0010 printed the first fifty dictionary entries and
    exited zero. (0010,0010), 0010,0010 and 00100010 are all accepted; junk is
    refused with a message saying what a tag looks like.

  • tag-doc with no arguments lists the dictionary, and writes no file. It
    announced "Displaying first 50 tags", listed none, printed three hardcoded counts,
    and created dicom_tags_text.txt in the working directory as a side effect — one of
    which reached this repository. It now lists real entries sorted by tag, honors
    -retired and -private, counts what it actually has, and writes to a file only
    when -output names one.

  • codify produces Go that compiles. Two defects: it emitted package main with
    no main function, 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
    through strconv.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, convert and
    codify accepted any file at all: show /etc/hosts printed a header, a column
    heading, no rows, and exited zero, and show /dev/null did 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 install builds it as go-dicom, after the last element of the module path, while
    the Makefile and the release assets called it dicom. 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-dicom
    

    The 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-arm64 and so on.

  • go-dicom help lists every command. It printed seven — the file commands —
    while the bare binary printed all sixteen grouped by category, because
    displayMainHelp kept 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 typing help. Both listings now come from one place and
    print identically. Found by rendering dicom help as a cover image and counting
    the commands on it.

  • help <command> works for every command. The top-level list advertised
    sixteen commands and help <name> knew seven of them: each of the nine network
    commands reported unknown command 'storescu', while storescu -h printed 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.sh works out the platform, downloads the matching build
    with the release's SHA256SUMS, and refuses to install unless the checksum matches.
    It prefers a directory already on PATH so 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 only go get of the library.

  • The README's command list covered eleven of the sixteen CLI commands. codify,
    echoscp, getscu, qrscp and tag-doc were 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
    ...
Read more

v1.4.0

Choose a tag to compare

@amrshadid amrshadid released this 31 Jul 22:40
5583a7d

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. NumberOfFrames is IS, and
    PS3.5 §6.2 pads a value to an even length with a trailing space, so "2 "
    failed strconv.Atoi and 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 in filereader, ignoring the byte order the reader had already been
      given. MR_small_bigendian.dcm parsed 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, so Dataset needs 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, and filewriter
      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.dcm now 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.

  • C-CANCEL aborted the association. SCU.Cancel sent 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 file input to the coverage upload was silently ignored in CI, and
    govulncheck was 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 through DecodePersonName and
    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 to ISO_IR 192 so 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 10 becomes 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-nooffset lost 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.

  • fileset searched nothing and counted wrongly. FindByModality,
    FindByPatient, FindByStudyInstanceUID and FindBySeriesInstanceUID ignored
    their argument and returned every record; GetStatistics reported the file
    count as the patient, study and series counts; ScanDirectory and AddFile
    never parsed the files they listed, so nothing above them had anything to work
    with; and GenerateDICOMDIR returned 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, so sizeof_hdr was 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 calls SetSupportedTransferSyntaxes.

  • 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
    StorageCommitmentHandler convenience 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.

  • commitscu CLI command, and network.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. filereader discarded 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 — the encaps package recovers them by parsing that
    structure, so ExtractEncapsulatedFrames failed with "failed to parse basic
    offset table" on every compressed file, and multi-frame images could not be
    split at all. PixelData now holds the encapsulation exactly as it appears in
    the file, matching what pydicom exposes.

    Verified against pydicom: MR_small_RLE.dcm gives one 6108-byte fragment
    from 6128 bytes of pixel data, and SC_rgb_rle_2frame.dcm splits 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...

Read more

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 21:49
b9b58e7

Upgrade from 1.2.0 without delay. The ItemTag constant 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 to SCUConfig.OnCStore and acknowledges each one.
    • CGetResponse.Instances supplies the instances to transfer
    • SCUConfig.OnCStore receives them on the requesting side
    • qrscp retains 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.Instances supplies the instances to move
    • QueryRetrieveHandler.OnMoveInstances returns them from a handler; the older
      OnMove, which could not transfer anything, is deprecated but still honored
    • SCPConfig.MoveDestinations and SCPConfig.ResolveMoveDestination resolve a
      destination AE title to an address; an unresolvable title is answered with
      StatusMoveDestUnknown
    • qrscp -move-dest AETITLE=host:port configures destinations from the CLI
  • Sequence writing in filewriterDataElement.Items holds nested
    SequenceItem values, 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 headerReadDICOMFile required 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 in DICOMFile.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 dcmtkscripts/interop-test.sh
    plus a CI job. Exercises C-ECHO and C-STORE in both directions and C-GET
    sub-operations, using pydicom's CT_small.dcm as 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 parsershow, info, convert, and
    codify used a parser in cli/helpers.go separate from filereader, 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 uses filereader,
    reports 269 elements for the same file, and indents sequence contents by nesting depth.
  • ItemTag was 0xFFFE0000, not 0xFFFEE000 (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.FromBytes and tag.ToBytes transposed 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 networkEncodeDataset skipped sequence elements,
    and storescu built its dataset from elem.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 allSendPData looped 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.OnGet results are now transferred rather than only counted.

Install

go install github.com/amrshadid/go-dicom@v1.3.0

Or 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 SHA256SUMS

Full changelog

v1.2.0

Choose a tag to compare

@amrshadid amrshadid released this 25 Jul 22:53
16547ef

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 headerDecodePDU sized its buffer from the peer-controlled 32-bit PDU length before reading. Now capped at MaxPDULengthLimit (128 MiB).
  • Oversized PDV allocationdecodeDataTF allocated 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 0xFFFFFFFF allocated 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.MaxAssociations was 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.OnGet had no HandleCGet method, so setting it silently did nothing.
  • SCU.NEventReport reported the wrong MessageIDRespondedTo and 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.
  • filewriter does not yet serialise SQ elements.
  • show / info / convert use 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

Choose a tag to compare

@amrshadid amrshadid released this 20 Mar 02:44
9f9a1ce

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.1

Download 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 SHA256SUMS

See CHANGELOG.md for complete release notes.

v1.1.0

Choose a tag to compare

@amrshadid amrshadid released this 18 Mar 23:13
57c50a1

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.0

Download 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 SHA256SUMS

See CHANGELOG.md for complete release notes.

v1.0.0

Choose a tag to compare

@amrshadid amrshadid released this 18 Mar 21:32

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.0

Download 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 SHA256SUMS

See CHANGELOG.md for complete release notes.