All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.8.7 - 2026-09-05
Changed
- Every association rejection now ends the process with a shutdown reason instead of a crash reason:
{:shutdown, :no_accepted_contexts},{:shutdown, :authentication_failed}and{:shutdown, :association_rejected}on the SCP side,{:shutdown, {:rejected, result, source, reason}}on the SCU side. Refusing an association is the decision the SCP was asked to make, so an SCP with an AE allowlist or an association policy no longer logs two error reports with a full state dump for every unauthorized attempt.Dimse.connect/3still returns{:error, {:rejected, result, source, reason}}and the other public results are unchanged. - Implementation version bumped to
DIMSE_0.8.7.
0.8.6 - 2026-09-05
Changed
- An association whose peer closes the TCP connection now exits with
{:shutdown, :tcp_closed}instead of:tcp_closed. Callers blocked on the association still receive{:error, :tcp_closed}andDimse.connect/3still returns{:error, :tcp_closed}, but GenServer and Ranch no longer log a crash for every TCP health probe that connects and closes without an A-ASSOCIATE-RQ (one error report per probe, every few seconds, on a monitored SCP). - Implementation version bumped to
DIMSE_0.8.6.
0.8.5 - 2026-09-05
Added
- Lazy C-MOVE and C-GET sub-operations.
handle_move/3andhandle_get/3may return{sop_class_uid, sop_instance_uid, fetcher}wherefetcheris a zero-arity function returning{:ok, data_set}or{:error, reason}; the association calls it right before that object's C-STORE and keeps no reference to the bytes afterwards, so a retrieve of thousands of objects holds one data set at a time. A fetcher that returns an error, raises, throws, exits or returns an unexpected term counts as one failed sub-operation (the last four also emit[:dimse, :handler, :exception]withcallback: :fetcher) and the retrieve continues. - The final C-MOVE-RSP / C-GET-RSP carries the Failed SOP Instance UID List (0008,0058) in sub-operation order when any sub-operation failed (PS3.4 C.4.2.1.4.2 and C.4.3.1.3.2), encoded in the negotiated transfer syntax; under Explicit VR Little Endian the list is cut to the UIDs that fit the 16-bit length while the counts stay exact.
- Implementation version bumped to
DIMSE_0.8.5.
Fixed
- Optional handler callbacks (
validate_association/2,handle_authenticate/2,supported_abstract_syntaxes/0,resolve_ae/1and the DIMSE-N SCP callbacks) were checked withfunction_exported?/3without loading the handler module first. In interactive mode (dev/test) a handler that had not been loaded yet silently lost those callbacks, so the first association could skip validation or authentication and be rejected with:no_accepted_contexts. The module is now loaded before the check.
0.8.4 - 2026-03-20
Changed
- Coverage increased to 98.16% overall, with
Dimse.Scudriven to 100% andDimse.Associationlifted to 95.64% - Added deterministic regression tests for SCU connect-exit normalization, DOWN-preferred error mapping, and internal association-helper edge cases
- Exposed a tiny set of
@doc falseSCU/association helpers to make shutdown and timeout branches testable without protocol flakiness - Implementation version bumped to
DIMSE_0.8.4
0.8.3 - 2026-03-20
Changed
- Coverage increased to 96.99% overall, with
Dimse.Scuraised through deterministic helper-path tests instead of race-dependent assertions - Extracted
Dimse.Scuconnection-finalization and exit-normalization helpers as documented internal APIs (@doc false) to keep timeout and shutdown semantics directly testable - Added regression tests for immediate-open timeout handling, connect-exit normalization, and DOWN-vs-fallback error mapping
- Implementation version bumped to
DIMSE_0.8.3
0.8.2 - 2026-03-19
Changed
- Coverage: 95.90% → 96.36% with targeted protocol edge-case tests
- Tests: 438 (10 properties + 428 tests)
- Added tests for unexpected PDU handling, A-ABORT during pending request, C-GET sub-op failure, raw TCP protocol edge cases
0.8.1 - 2026-03-19
Added
validate_association/2optional handler callback for pre-auth association admission control- Current presentation-context details in handler state via
current_context_id,current_abstract_syntax_uid, andcurrent_transfer_syntax_uid - Integration coverage for association rejection and callback context propagation
Changed
- SCP handler callbacks now receive association state enriched with the negotiated presentation context for the current message
- Association rejection can now happen before user-identity authentication, enabling AE-title and policy checks at admission time
0.8.0 - 2026-03-18
Added
Dimse.with_connection/4— automatic connect/execute/release lifecycleDimse.Errormodule — error taxonomy with types for all 4 error categoriesDimse.Tlsmodule — shared TLS option normalization- Expanded telemetry: 17 events across 6 categories (negotiation, TLS handshake, handler callbacks, sub-operations)
- TLS hardening tests: hostname verification (SNI), handshake timeout, error surfacing, wrong-protocol detection
- Interop test infrastructure with Docker harness (dcmtk, Orthanc, pynetdicom)
- Protocol edge-case tests: 128 contexts, fragmentation boundaries, concurrent associations, ARTIM timer
artim_timeoutoption forstart_listener/1
Changed
- DRY: extracted
put_if/3in Dimse.Scu (wasmaybe_put/3duplicated in store, n_get, n_create) - DRY: extracted
Dimse.Tls.normalize_opts/1(wasnormalize_tls_opt/1duplicated in association, listener) - Implementation version bumped to
DIMSE_0.8.0 - Test count: 340 → 429 (10 properties + 419 tests), coverage 95.79% → 96%+
Removed
docs/roadmap.md
0.7.1 - 2026-03-18
Added
- 17 additional tests for default-argument stubs, abort paths, and tcp_closed scenarios; total 340 tests, 95.79% coverage
Fixed
reverse_ui_list_fields/1fast path: skips struct rebuild for plain associations without Extended Negotiation sub-items (common case)
Changed
- README trimmed and updated with block-letter logo
- Removed stale
docs/PRD.md
0.7.0 - 2026-03-18
Added
- Extended Negotiation sub-items in A-ASSOCIATE-RQ/AC
UserInformationblock (PS3.7 Annex D) - Role Selection (0x54) — SCU/SCP role negotiation per SOP class (
Dimse.Pdu.RoleSelection) - SOP Class Extended Negotiation (0x56) — service-class-specific application info (
Dimse.Pdu.SopClassExtendedNegotiation) - SOP Class Common Extended Negotiation (0x57) — service class UID + related SOP class UIDs (
Dimse.Pdu.SopClassCommonExtendedNegotiation) - User Identity Negotiation (0x58/0x59) — username/password/Kerberos/SAML/JWT authentication (
Dimse.Pdu.UserIdentity,Dimse.Pdu.UserIdentityAc) handle_authenticate/2optional SCP callback — returns{:ok, nil | binary()}to accept or{:error, reason}to reject; A-ASSOCIATE-RJ sent automatically on rejectionDimse.Association.negotiated_roles/1— retrieve negotiated SCU/SCP roles for the association as%{sop_class_uid => {scu, scp}}:role_selectionsand:user_identityopts forDimse.connect/3andDimse.Scu.open/3- SCP echoes back role selections filtered to accepted SOP classes in A-ASSOCIATE-AC
- Server response from
handle_authenticate/2included in A-ASSOCIATE-AC when SCU requested positive response - 45 new tests: 23 PDU unit tests + 5 Extended Negotiation integration tests + 17 coverage tests for default-arg stubs, abort paths, and tcp_closed scenarios
Changed
UserInformationstruct replacesextended_negotiation: term()placeholder with 5 typed fields:role_selections,sop_class_extended,sop_class_common_extended,user_identity,user_identity_achandle_authenticate/2added to@optional_callbacks— associations without the callback default to accepting all SCUs- Implementation version bumped to
DIMSE_0.7.0
0.6.1 - 2026-03-18
Added
bench/directory with five Benchee suites: PDU encode/decode, command encode/decode, message fragmentation/assembly, persistent-connection throughput, and end-to-end round-trip benchmarksbencheedev dependency for the benchmark suites
Changed
Dimse.connect/3andDimse.Scu.open/3now enforce a single timeout budget across socket connect plus association negotiation- PDU encoder keeps iodata throughout — no intermediate
IO.iodata_to_binaryflatten; sizes computed via:erlang.iolist_size/1 - PDU encoder fast-path for single-PDV
P-DATA-TF(the common case) avoidsEnum.maplist allocation PDU decoder accumulates presentation contexts with
[h | t]+Enum.reverse/1instead of++ [item](O(n) vs O(n²))- Command encoder uses compile-time pattern-match dispatch for VR lookup instead of
Map.get/3at runtime - Message fragmentation rewritten as a tail-recursive accumulator — no intermediate chunk lists, direct struct construction
AssociationTCP receive handler skips binary concatenation when the buffer is already empty (common case: one PDU per segment)
Fixed
- SCP examples now declare
supported_abstract_syntaxes/0for the non-Verification services they advertise - TLS failure-path tests now require synchronous
connect/3failure instead of tolerating the old async-connect behavior
0.6.0 - 2026-03-18
Added
- TLS / DICOM Secure Transport (PS3.15 Annex B) support for both SCP and SCU
start_listener/1accepts:tlsoption to useranch_ssltransportconnect/3accepts:tlsoption to connect via:sslinstead of:gen_tcp- Full mutual TLS (mTLS) support: SCP can require client certificates
- All standard
:ssloptions (:certfile,:keyfile,:cacertfile,:verify,:fail_if_no_peer_cert) passed through to OTP :ssland:public_keyadded toextra_applications(OTP stdlib, zero new deps)- 7 TLS integration tests: C-ECHO over TLS, C-STORE over TLS, mutual TLS, DIMSE-N over TLS, mixed TCP+TLS listeners, untrusted CA rejection, missing client cert rejection
- Dynamic test certificate generation using OTP
:public_key(no cert files in repo)
Changed
handle_info/2now uses guards (when proto in [:tcp, :ssl]) instead of atom-specific clauses for socket messagessend_pdu/2,reactivate_socket/1, andclose_socket/1handle:ssltransport alongside:gen_tcpand Ranch transports- Implementation version bumped to
DIMSE_0.6.0
Fixed
Dimse.connect/3andDimse.Scu.open/3now wait for association negotiation to succeed or fail before returningDimse.n_create/4now returns the created SOP Instance UID alongside status and response dataN-CREATErequests without an Attribute List now setCommandDataSetTypecorrectly
0.5.1 - 2026-03-18
Fixed
N-GETnow correctly encodes and decodes multi-valued Attribute Identifier Lists instead of crashing on a documented list inputN-CREATESCP handlers can now return the created SOP Instance UID for inclusion in the response command- DIMSE-N SCU helpers now return explicit error tuples for DIMSE failure statuses instead of reporting them as
{:ok, status, data}
0.5.0 - 2026-03-18
Added
- All 6 DIMSE-N services (PS3.7 Chapter 10): N-EVENT-REPORT, N-GET, N-SET, N-ACTION, N-CREATE, N-DELETE
- SCU modules:
Dimse.Scu.NGet,Dimse.Scu.NSet,Dimse.Scu.NAction,Dimse.Scu.NCreate,Dimse.Scu.NDelete,Dimse.Scu.NEventReport - Public API:
Dimse.n_get/4,Dimse.n_set/5,Dimse.n_action/6,Dimse.n_create/4,Dimse.n_delete/4,Dimse.n_event_report/6 - 6 optional handler callbacks:
handle_n_get/2,handle_n_set/3,handle_n_action/3,handle_n_create/3,handle_n_delete/2,handle_n_event_report/3 - SCP dispatch for all 6 DIMSE-N command fields with
function_exported?/3fallback (0x0112 No Such SOP Class) - Correct Requested vs Affected SOP Class/Instance UID handling per PS3.7 Table 10.1
- DIMSE-N response builder with data set support (CommandDataSetType 0x0000 when data present)
- Extra response tags: AffectedSOPInstanceUID, EventTypeID, ActionTypeID echoed in N-*-RSP
- 35 SCU unit tests (build_command_set for all 6 services)
- 9 integration tests: N-GET, N-SET, N-ACTION, N-CREATE, N-DELETE, N-EVENT-REPORT round trips, error handling, mixed DIMSE-C+N, Storage Commitment flow
Changed
send_dimse_request/4now checks both AffectedSOPClassUID (0000,0002) and RequestedSOPClassUID (0000,0003) for context lookuprequest_on_negotiated_context?/2validates both Affected and Requested SOP Class UIDs- Response builder dynamically sets CommandDataSetType based on response data presence (was hardcoded 0x0101)
0.4.1 - 2026-03-18
Added
- C-MOVE SCU (
Dimse.Scu.Move) — retrieve instances to a destination AE via C-STORE sub-operations - C-GET SCU (
Dimse.Scu.Get) — retrieve instances on the same association via interleaved C-STORE sub-ops Dimse.move/4public API with query level convenience atoms (:patient,:study) anddest_aeoptionDimse.get/4public API with query level convenience atoms (:patient,:study)Association.get_request/4for C-GET multi-response with interleaved C-STORE handlingresolve_ae/1optional handler callback for C-MOVE destination AE resolution- Sub-operation state machine in
Association.Statefor tracking C-GET/C-MOVE progress - SCP dispatch for C-GET-RQ (0x0010) and C-MOVE-RQ (0x0021) with asynchronous sub-operation processing
- SCU
get_modefor auto-accepting C-STORE sub-operations during C-GET retrieval - Sub-operation count tracking (remaining/completed/failed/warning) in C-GET/C-MOVE responses
- C-GET integration tests (basic retrieval, empty, error, mixed echo+get)
- C-MOVE integration tests (basic retrieval, empty, error, resolve_ae failure, mixed echo+move)
- Unit tests for
Dimse.Scu.MoveandDimse.Scu.Get(SOP class mapping, command set building)
Changed
- Breaking:
handle_move/3now returns{:ok, [{sop_class_uid, sop_instance_uid, data}]}(was{:ok, [String.t()]}) - Breaking:
handle_get/3now returns{:ok, [{sop_class_uid, sop_instance_uid, data}]}(was{:ok, [binary()]}) MoveOriginatorMessageIDtag (0000,1031) added to command VR map for correct US encoding
Fixed
- SCU requests now fail closed when no negotiated presentation context matches the requested SOP Class
Dimse.find/4now returns an explicit cancellation error when a peer ends the operation with status0xFE00
0.3.0 - 2026-03-18
Added
- C-FIND SCU (
Dimse.Scu.Find) — query DICOM data sets from remote SCPs Dimse.find/4public API with query level convenience atoms (:patient,:study,:worklist)Dimse.cancel/2for C-CANCEL-RQ (PS3.7 §9.3.2.3)- Multi-response handling in Association for Pending + Success response sequences
Association.find_request/4for multi-response DIMSE commands- Query SOP Class UID mapping (
Dimse.Scu.Find.sop_class_uid/1) - C-FIND integration tests (basic, empty, many results, error, mixed operations, cancel)
- Late response tolerance — SCU ignores responses after operation completes
Changed
- SCP C-CANCEL-RQ handling — no longer sends erroneous response to C-CANCEL
0.2.0 - 2026-03-18
Added
- C-STORE SCU (
Dimse.Scu.Store) — send DICOM instances to remote SCPs Dimse.store/5public API with priority and move originator support- Command-level telemetry events (
[:dimse, :command_start],[:dimse, :command_stop]) AffectedSOPInstanceUIDecho-back in C-STORE-RSP (PS3.7 Table 9.1-1)- Property-based tests using StreamData for PDU encode/decode roundtrips
@type t()specs on all PDU structs (fixesmix docswarnings)- StreamData generators in test helpers for all PDU types
- C-STORE integration tests (basic, multiple, large data set fragmentation, mixed echo+store)
0.1.0 - 2025-12-15
Added
- PDU type structs for all 7 DICOM Upper Layer PDU types (PS3.8 Section 9.3)
- PDU encoder (struct → iodata) and decoder (binary → struct)
- DIMSE command set encode/decode (Implicit VR Little Endian, group 0000)
- DIMSE command field constants (PS3.7 Annex E)
- DIMSE status code constants and classification (PS3.7 Annex C)
- Association GenServer with 5-phase state machine (PS3.8 Section 9.2)
- ARTIM timer for connection timeout compliance
- Presentation context negotiation (abstract syntax + transfer syntax matching)
- Max PDU length negotiation with automatic message fragmentation
- Handler behaviour (
Dimse.Handler) for SCP service class implementations - Built-in C-ECHO SCP and SCU
- SCU client API (
Dimse.Scu.open/3, release, abort) - Ranch TCP acceptor integration
- Telemetry event definitions for association lifecycle and PDU I/O
- CI workflow with Elixir 1.16/1.17/1.18 matrix