PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::network::pdu_decoder Class Reference

Decoder for DICOM PDU (Protocol Data Unit) messages. More...

#include <pdu_decoder.h>

Collaboration diagram for kcenon::pacs::network::pdu_decoder:
Collaboration graph

Static Public Member Functions

General Decoding
static DecodeResult< pdu > decode (std::span< const uint8_t > data)
 Decode any PDU from bytes.
 
static std::optional< size_t > pdu_length (std::span< const uint8_t > data)
 Check if a complete PDU is available in the buffer.
 
static std::optional< pdu_type > peek_pdu_type (std::span< const uint8_t > data)
 Get the PDU type from buffer without full decoding.
 
Specific Decoders
static DecodeResult< associate_rq > decode_associate_rq (std::span< const uint8_t > data)
 Decode an A-ASSOCIATE-RQ PDU.
 
static DecodeResult< associate_ac > decode_associate_ac (std::span< const uint8_t > data)
 Decode an A-ASSOCIATE-AC PDU.
 
static DecodeResult< associate_rj > decode_associate_rj (std::span< const uint8_t > data)
 Decode an A-ASSOCIATE-RJ PDU.
 
static DecodeResult< p_data_tf_pdu > decode_p_data_tf (std::span< const uint8_t > data)
 Decode a P-DATA-TF PDU.
 
static DecodeResult< release_rq_pdu > decode_release_rq (std::span< const uint8_t > data)
 Decode an A-RELEASE-RQ PDU.
 
static DecodeResult< release_rp_pdu > decode_release_rp (std::span< const uint8_t > data)
 Decode an A-RELEASE-RP PDU.
 
static DecodeResult< abort_pdu > decode_abort (std::span< const uint8_t > data)
 Decode an A-ABORT PDU.
 

Static Private Member Functions

Helper Functions
static uint16_t read_uint16_be (std::span< const uint8_t > data, size_t offset)
 Read a 16-bit unsigned integer in big-endian format.
 
static uint32_t read_uint32_be (std::span< const uint8_t > data, size_t offset)
 Read a 32-bit unsigned integer in big-endian format.
 
static std::string read_ae_title (std::span< const uint8_t > data, size_t offset)
 Read an AE Title (16 bytes, space-trimmed).
 
static std::string read_uid (std::span< const uint8_t > data, size_t offset, size_t length)
 Read a UID string (trim trailing null/space padding).
 
static DecodeResult< uint32_t > validate_pdu_header (std::span< const uint8_t > data, uint8_t expected_type=0)
 Validate PDU header and extract length.
 
static DecodeResult< std::tuple< std::string, std::vector< presentation_context_rq >, std::vector< presentation_context_ac >, user_information > > decode_variable_items (std::span< const uint8_t > data, bool is_rq)
 Decode variable items from ASSOCIATE-RQ/AC PDUs.
 
static DecodeResult< user_information > decode_user_info_item (std::span< const uint8_t > data)
 Decode User Information sub-items.
 

Detailed Description

Decoder for DICOM PDU (Protocol Data Unit) messages.

This class provides static methods to decode various PDU types according to DICOM PS3.8 Upper Layer Protocol.

PDU Structure:

┌─────────────────────────────────────┐
│ PDU Header │
├───────────┬───────────┬─────────────┤
│ Type │ Reserved │ Length │
│ (1 byte) │ (1 byte) │ (4 bytes) │
└───────────┴───────────┴─────────────┘
│ PDU Data (variable) │
└─────────────────────────────────────┘
See also
DICOM PS3.8 Section 9 - Upper Layer Protocol

Definition at line 127 of file pdu_decoder.h.

Member Function Documentation

◆ decode()

DecodeResult< pdu > kcenon::pacs::network::pdu_decoder::decode ( std::span< const uint8_t > data)
staticnodiscard

Decode any PDU from bytes.

Parameters
dataInput byte buffer
Returns
Result containing decoded PDU or error

Automatically detects PDU type from the first byte and dispatches to the appropriate specific decoder.

Definition at line 178 of file pdu_decoder.cpp.

178 {
179 if (data.size() < PDU_HEADER_SIZE) {
180 return make_error<pdu>(pdu_decode_error::incomplete_header);
181 }
182
183 const uint8_t type = data[0];
184
185 switch (type) {
186 case 0x01: {
187 auto result = decode_associate_rq(data);
188 if (result.is_ok()) {
189 return make_ok<pdu>(std::move(result.value()));
190 }
191 return result.error();
192 }
193 case 0x02: {
194 auto result = decode_associate_ac(data);
195 if (result.is_ok()) {
196 return make_ok<pdu>(std::move(result.value()));
197 }
198 return result.error();
199 }
200 case 0x03: {
201 auto result = decode_associate_rj(data);
202 if (result.is_ok()) {
203 return make_ok<pdu>(std::move(result.value()));
204 }
205 return result.error();
206 }
207 case 0x04: {
208 auto result = decode_p_data_tf(data);
209 if (result.is_ok()) {
210 return make_ok<pdu>(std::move(result.value()));
211 }
212 return result.error();
213 }
214 case 0x05: {
215 auto result = decode_release_rq(data);
216 if (result.is_ok()) {
217 return make_ok<pdu>(std::move(result.value()));
218 }
219 return result.error();
220 }
221 case 0x06: {
222 auto result = decode_release_rp(data);
223 if (result.is_ok()) {
224 return make_ok<pdu>(std::move(result.value()));
225 }
226 return result.error();
227 }
228 case 0x07: {
229 auto result = decode_abort(data);
230 if (result.is_ok()) {
231 return make_ok<pdu>(std::move(result.value()));
232 }
233 return result.error();
234 }
235 default:
236 return make_error<pdu>(pdu_decode_error::invalid_pdu_type,
237 "Unknown PDU type: " + std::to_string(type));
238 }
239}
static DecodeResult< associate_ac > decode_associate_ac(std::span< const uint8_t > data)
Decode an A-ASSOCIATE-AC PDU.
static DecodeResult< associate_rq > decode_associate_rq(std::span< const uint8_t > data)
Decode an A-ASSOCIATE-RQ PDU.
static DecodeResult< release_rp_pdu > decode_release_rp(std::span< const uint8_t > data)
Decode an A-RELEASE-RP PDU.
static DecodeResult< abort_pdu > decode_abort(std::span< const uint8_t > data)
Decode an A-ABORT PDU.
static DecodeResult< release_rq_pdu > decode_release_rq(std::span< const uint8_t > data)
Decode an A-RELEASE-RQ PDU.
static DecodeResult< associate_rj > decode_associate_rj(std::span< const uint8_t > data)
Decode an A-ASSOCIATE-RJ PDU.
static DecodeResult< p_data_tf_pdu > decode_p_data_tf(std::span< const uint8_t > data)
Decode a P-DATA-TF PDU.
@ incomplete_header
Less than 6 bytes available.
@ invalid_pdu_type
Unknown PDU type byte.

References decode_abort(), decode_associate_ac(), decode_associate_rj(), decode_associate_rq(), decode_p_data_tf(), decode_release_rp(), decode_release_rq(), kcenon::pacs::network::incomplete_header, and kcenon::pacs::network::invalid_pdu_type.

Here is the call graph for this function:

◆ decode_abort()

DecodeResult< abort_pdu > kcenon::pacs::network::pdu_decoder::decode_abort ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-ABORT PDU.

Parameters
dataInput byte buffer
Returns
Result containing abort_pdu or error

Definition at line 300 of file pdu_decoder.cpp.

300 {
301 auto header_result = validate_pdu_header(data, 0x07);
302 if (header_result.is_err()) {
303 return header_result.error();
304 }
305
306 if (data.size() < FIXED_PDU_SIZE) {
307 return make_error<abort_pdu>(pdu_decode_error::incomplete_pdu);
308 }
309
310 abort_pdu abort;
311 abort.source = static_cast<abort_source>(data[8]);
312 abort.reason = static_cast<abort_reason>(data[9]);
313
314 return make_ok(std::move(abort));
315}
static DecodeResult< uint32_t > validate_pdu_header(std::span< const uint8_t > data, uint8_t expected_type=0)
Validate PDU header and extract length.
@ incomplete_pdu
PDU length exceeds available data.

References kcenon::pacs::network::abort, kcenon::pacs::network::incomplete_pdu, kcenon::pacs::network::abort_pdu::source, and validate_pdu_header().

Referenced by decode().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_associate_ac()

DecodeResult< associate_ac > kcenon::pacs::network::pdu_decoder::decode_associate_ac ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-ASSOCIATE-AC PDU.

Parameters
dataInput byte buffer
Returns
Result containing associate_ac or error

Definition at line 664 of file pdu_decoder.cpp.

665 {
666
667 auto header_result = validate_pdu_header(data, 0x02);
668 if (header_result.is_err()) {
669 return header_result.error();
670 }
671
672 const uint32_t pdu_length = header_result.value();
673
674 // Check minimum size
675 if (pdu_length < ASSOCIATE_HEADER_SIZE) {
676 return make_error<associate_ac>(pdu_decode_error::malformed_pdu,
677 "PDU too short for ASSOCIATE-AC");
678 }
679
680 // Check protocol version
681 const uint16_t protocol_version = read_uint16_be(data, 6);
682 if (protocol_version != DICOM_PROTOCOL_VERSION) {
683 return make_error<associate_ac>(pdu_decode_error::invalid_protocol_version,
684 "Expected version 1, got " + std::to_string(protocol_version));
685 }
686
687 associate_ac ac;
688
689 // Called AE Title
690 ac.called_ae_title = read_ae_title(data, 10);
691
692 // Calling AE Title
693 ac.calling_ae_title = read_ae_title(data, 26);
694
695 // Variable items
696 const size_t variable_start = PDU_HEADER_SIZE + ASSOCIATE_HEADER_SIZE;
697 const size_t variable_length = pdu_length - ASSOCIATE_HEADER_SIZE;
698
699 if (variable_length > 0 && variable_start + variable_length <= data.size()) {
700 auto var_result = decode_variable_items(
701 data.subspan(variable_start, variable_length), false);
702
703 if (var_result.is_err()) {
704 return var_result.error();
705 }
706
707 auto& [app_ctx, pcs_rq, pcs_ac, user_info] = var_result.value();
708 ac.application_context = std::move(app_ctx);
709 ac.presentation_contexts = std::move(pcs_ac);
710 ac.user_info = std::move(user_info);
711 }
712
713 return make_ok(std::move(ac));
714}
static DecodeResult< std::tuple< std::string, std::vector< presentation_context_rq >, std::vector< presentation_context_ac >, user_information > > decode_variable_items(std::span< const uint8_t > data, bool is_rq)
Decode variable items from ASSOCIATE-RQ/AC PDUs.
static uint16_t read_uint16_be(std::span< const uint8_t > data, size_t offset)
Read a 16-bit unsigned integer in big-endian format.
static std::string read_ae_title(std::span< const uint8_t > data, size_t offset)
Read an AE Title (16 bytes, space-trimmed).
static std::optional< size_t > pdu_length(std::span< const uint8_t > data)
Check if a complete PDU is available in the buffer.
@ associate_ac
A-ASSOCIATE-AC (Association Accept)
constexpr uint16_t DICOM_PROTOCOL_VERSION
DICOM Protocol Version.
Definition pdu_types.h:270
@ malformed_pdu
PDU structure is invalid.
@ invalid_protocol_version
Unsupported protocol version.

References kcenon::pacs::network::associate_ac::application_context, kcenon::pacs::network::associate_ac::called_ae_title, kcenon::pacs::network::associate_ac::calling_ae_title, decode_variable_items(), kcenon::pacs::network::DICOM_PROTOCOL_VERSION, kcenon::pacs::network::invalid_protocol_version, kcenon::pacs::network::malformed_pdu, pdu_length(), kcenon::pacs::network::associate_ac::presentation_contexts, read_ae_title(), read_uint16_be(), kcenon::pacs::network::associate_ac::user_info, and validate_pdu_header().

Referenced by decode().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_associate_rj()

DecodeResult< associate_rj > kcenon::pacs::network::pdu_decoder::decode_associate_rj ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-ASSOCIATE-RJ PDU.

Parameters
dataInput byte buffer
Returns
Result containing associate_rj or error

Definition at line 245 of file pdu_decoder.cpp.

246 {
247
248 auto header_result = validate_pdu_header(data, 0x03);
249 if (header_result.is_err()) {
250 return header_result.error();
251 }
252
253 // A-ASSOCIATE-RJ is always 10 bytes
254 if (data.size() < FIXED_PDU_SIZE) {
255 return make_error<associate_rj>(pdu_decode_error::incomplete_pdu);
256 }
257
258 associate_rj rj;
259 rj.result = static_cast<reject_result>(data[7]);
260 rj.source = data[8];
261 rj.reason = data[9];
262
263 return make_ok(std::move(rj));
264}
reject_result
Reject result values.
Definition pdu_types.h:92
@ associate_rj
A-ASSOCIATE-RJ (Association Reject)

References kcenon::pacs::network::incomplete_pdu, kcenon::pacs::network::associate_rj::reason, kcenon::pacs::network::associate_rj::result, kcenon::pacs::network::associate_rj::source, and validate_pdu_header().

Referenced by decode().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_associate_rq()

DecodeResult< associate_rq > kcenon::pacs::network::pdu_decoder::decode_associate_rq ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-ASSOCIATE-RQ PDU.

Parameters
dataInput byte buffer
Returns
Result containing associate_rq or error

Definition at line 606 of file pdu_decoder.cpp.

607 {
608
609 auto header_result = validate_pdu_header(data, 0x01);
610 if (header_result.is_err()) {
611 return header_result.error();
612 }
613
614 const uint32_t pdu_length = header_result.value();
615
616 // Check minimum size for ASSOCIATE header
617 if (pdu_length < ASSOCIATE_HEADER_SIZE) {
618 return make_error<associate_rq>(pdu_decode_error::malformed_pdu,
619 "PDU too short for ASSOCIATE-RQ");
620 }
621
622 // Check protocol version (bytes 6-7)
623 const uint16_t protocol_version = read_uint16_be(data, 6);
624 if (protocol_version != DICOM_PROTOCOL_VERSION) {
625 return make_error<associate_rq>(pdu_decode_error::invalid_protocol_version,
626 "Expected version 1, got " + std::to_string(protocol_version));
627 }
628
629 associate_rq rq;
630
631 // Called AE Title (bytes 10-25)
632 rq.called_ae_title = read_ae_title(data, 10);
633
634 // Calling AE Title (bytes 26-41)
635 rq.calling_ae_title = read_ae_title(data, 26);
636
637 // Reserved bytes 42-73 (32 bytes)
638 // Variable items start at byte 74
639
640 const size_t variable_start = PDU_HEADER_SIZE + ASSOCIATE_HEADER_SIZE;
641 const size_t variable_length = pdu_length - ASSOCIATE_HEADER_SIZE;
642
643 if (variable_length > 0 && variable_start + variable_length <= data.size()) {
644 auto var_result = decode_variable_items(
645 data.subspan(variable_start, variable_length), true);
646
647 if (var_result.is_err()) {
648 return var_result.error();
649 }
650
651 auto& [app_ctx, pcs_rq, pcs_ac, user_info] = var_result.value();
652 rq.application_context = std::move(app_ctx);
653 rq.presentation_contexts = std::move(pcs_rq);
654 rq.user_info = std::move(user_info);
655 }
656
657 return make_ok(std::move(rq));
658}
@ associate_rq
A-ASSOCIATE-RQ (Association Request)

References kcenon::pacs::network::associate_rq::application_context, kcenon::pacs::network::associate_rq::called_ae_title, kcenon::pacs::network::associate_rq::calling_ae_title, decode_variable_items(), kcenon::pacs::network::DICOM_PROTOCOL_VERSION, kcenon::pacs::network::invalid_protocol_version, kcenon::pacs::network::malformed_pdu, pdu_length(), kcenon::pacs::network::associate_rq::presentation_contexts, read_ae_title(), read_uint16_be(), kcenon::pacs::network::associate_rq::user_info, and validate_pdu_header().

Referenced by decode(), and kcenon::pacs::network::v2::dicom_association_handler::handle_associate_rq().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_p_data_tf()

DecodeResult< p_data_tf_pdu > kcenon::pacs::network::pdu_decoder::decode_p_data_tf ( std::span< const uint8_t > data)
staticnodiscard

Decode a P-DATA-TF PDU.

Parameters
dataInput byte buffer
Returns
Result containing p_data_tf_pdu or error

Definition at line 321 of file pdu_decoder.cpp.

322 {
323
324 auto header_result = validate_pdu_header(data, 0x04);
325 if (header_result.is_err()) {
326 return header_result.error();
327 }
328
329 const uint32_t pdu_length = header_result.value();
330 const size_t pdu_end = PDU_HEADER_SIZE + pdu_length;
331
332 p_data_tf_pdu result;
333 size_t pos = PDU_HEADER_SIZE;
334
335 while (pos < pdu_end) {
336 // Each PDV item has:
337 // - 4-byte length
338 // - 1-byte presentation context ID
339 // - 1-byte message control header
340 // - variable data
341
342 if (pos + 4 > pdu_end) {
343 return make_error<p_data_tf_pdu>(pdu_decode_error::malformed_pdu,
344 "Incomplete PDV item length");
345 }
346
347 const uint32_t pdv_item_length = read_uint32_be(data, pos);
348 pos += 4;
349
350 if (pdv_item_length < 2) {
351 return make_error<p_data_tf_pdu>(pdu_decode_error::malformed_pdu,
352 "PDV item length too small");
353 }
354
355 // Subtract first to avoid pos + pdv_item_length wrapping on 32-bit
356 // platforms when pdv_item_length is attacker-controlled.
357 if (pdv_item_length > pdu_end - pos) {
358 return make_error<p_data_tf_pdu>(pdu_decode_error::buffer_overflow,
359 "PDV item exceeds PDU bounds");
360 }
361
362 presentation_data_value pdv;
363 pdv.context_id = data[pos];
364 pos += 1;
365
366 const uint8_t control = data[pos];
367 pos += 1;
368
369 pdv.is_command = (control & 0x01) != 0;
370 pdv.is_last = (control & 0x02) != 0;
371
372 // Data length = item length - context_id (1) - control (1)
373 const size_t data_length = pdv_item_length - 2;
374 pdv.data.assign(data.begin() + static_cast<ptrdiff_t>(pos),
375 data.begin() + static_cast<ptrdiff_t>(pos + data_length));
376 pos += data_length;
377
378 result.pdvs.push_back(std::move(pdv));
379 }
380
381 return make_ok(std::move(result));
382}
static uint32_t read_uint32_be(std::span< const uint8_t > data, size_t offset)
Read a 32-bit unsigned integer in big-endian format.
@ control
Internal pipeline control messages.
@ buffer_overflow
Item length exceeds PDU bounds.

References kcenon::pacs::network::buffer_overflow, kcenon::pacs::network::presentation_data_value::context_id, kcenon::pacs::network::presentation_data_value::data, kcenon::pacs::network::presentation_data_value::is_command, kcenon::pacs::network::presentation_data_value::is_last, kcenon::pacs::network::malformed_pdu, pdu_length(), kcenon::pacs::network::p_data_tf_pdu::pdvs, read_uint32_be(), and validate_pdu_header().

Referenced by decode(), and kcenon::pacs::network::v2::dicom_association_handler::handle_p_data_tf().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_release_rp()

DecodeResult< release_rp_pdu > kcenon::pacs::network::pdu_decoder::decode_release_rp ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-RELEASE-RP PDU.

Parameters
dataInput byte buffer
Returns
Result containing release_rp_pdu or error

Definition at line 285 of file pdu_decoder.cpp.

286 {
287
288 auto header_result = validate_pdu_header(data, 0x06);
289 if (header_result.is_err()) {
290 return header_result.error();
291 }
292
293 return make_ok(release_rp_pdu{});
294}

References validate_pdu_header().

Referenced by decode().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_release_rq()

DecodeResult< release_rq_pdu > kcenon::pacs::network::pdu_decoder::decode_release_rq ( std::span< const uint8_t > data)
staticnodiscard

Decode an A-RELEASE-RQ PDU.

Parameters
dataInput byte buffer
Returns
Result containing release_rq_pdu or error

Definition at line 270 of file pdu_decoder.cpp.

271 {
272
273 auto header_result = validate_pdu_header(data, 0x05);
274 if (header_result.is_err()) {
275 return header_result.error();
276 }
277
278 return make_ok(release_rq_pdu{});
279}

References validate_pdu_header().

Referenced by decode().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_user_info_item()

DecodeResult< user_information > kcenon::pacs::network::pdu_decoder::decode_user_info_item ( std::span< const uint8_t > data)
staticnodiscardprivate

Decode User Information sub-items.

Definition at line 538 of file pdu_decoder.cpp.

539 {
540
542 size_t pos = 0;
543
544 while (pos < data.size()) {
545 if (pos + 4 > data.size()) break;
546
547 const uint8_t sub_type = data[pos];
548 // Reserved byte at pos + 1
549 const uint16_t sub_length = read_uint16_be(data, pos + 2);
550 pos += 4;
551
552 if (pos + sub_length > data.size()) break;
553
554 switch (sub_type) {
555 case 0x51: // Maximum Length
556 if (sub_length >= 4) {
557 ui.max_pdu_length = read_uint32_be(data, pos);
558 }
559 break;
560
561 case 0x52: // Implementation Class UID
562 ui.implementation_class_uid = read_uid(data, pos, sub_length);
563 break;
564
565 case 0x55: // Implementation Version Name
566 ui.implementation_version_name = read_uid(data, pos, sub_length);
567 break;
568
569 case 0x54: { // SCP/SCU Role Selection
570 if (sub_length >= 4) {
571 const uint16_t uid_length = read_uint16_be(data, pos);
572 // UID is padded to even length in encoder. Compute in a
573 // wider type to avoid uint16_t overflow when uid_length is
574 // odd and near its maximum.
575 const size_t padded_uid_length =
576 static_cast<size_t>(uid_length) +
577 (static_cast<size_t>(uid_length) % 2);
578 // Bounds check: UID length field (2) + padded UID bytes +
579 // SCU role (1) + SCP role (1) must fit inside sub_length.
580 if (padded_uid_length + 4 <= sub_length) {
582 role.sop_class_uid = read_uid(data, pos + 2, uid_length);
583 role.scu_role = (data[pos + 2 + padded_uid_length] != 0);
584 role.scp_role = (data[pos + 2 + padded_uid_length + 1] != 0);
585 ui.role_selections.push_back(std::move(role));
586 }
587 }
588 break;
589 }
590
591 default:
592 // Skip unknown sub-items
593 break;
594 }
595
596 pos += sub_length;
597 }
598
599 return make_ok(std::move(ui));
600}
static std::string read_uid(std::span< const uint8_t > data, size_t offset, size_t length)
Read a UID string (trim trailing null/space padding).
@ scp_scu_role_selection
SCP/SCU Role Selection Sub-item.
@ user_information
User Information Item.

References kcenon::pacs::network::user_information::implementation_class_uid, kcenon::pacs::network::user_information::implementation_version_name, kcenon::pacs::network::user_information::max_pdu_length, read_uid(), read_uint16_be(), read_uint32_be(), kcenon::pacs::network::user_information::role_selections, and kcenon::pacs::network::scp_scu_role_selection::sop_class_uid.

Referenced by decode_variable_items().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ decode_variable_items()

DecodeResult< std::tuple< std::string, std::vector< presentation_context_rq >, std::vector< presentation_context_ac >, user_information > > kcenon::pacs::network::pdu_decoder::decode_variable_items ( std::span< const uint8_t > data,
bool is_rq )
staticnodiscardprivate

Decode variable items from ASSOCIATE-RQ/AC PDUs.

Definition at line 393 of file pdu_decoder.cpp.

393 {
394 std::string application_context;
395 std::vector<presentation_context_rq> pcs_rq;
396 std::vector<presentation_context_ac> pcs_ac;
397 user_information user_info;
398
399 size_t pos = 0;
400
401 while (pos < data.size()) {
402 if (pos + 4 > data.size()) {
403 return make_error<std::tuple<std::string,
404 std::vector<presentation_context_rq>,
405 std::vector<presentation_context_ac>,
407 "Incomplete item header");
408 }
409
410 const uint8_t item_type_byte = data[pos];
411 // Reserved byte at pos + 1
412 const uint16_t item_length = read_uint16_be(data, pos + 2);
413 pos += 4;
414
415 if (pos + item_length > data.size()) {
416 return make_error<std::tuple<std::string,
417 std::vector<presentation_context_rq>,
418 std::vector<presentation_context_ac>,
420 "Item length exceeds buffer");
421 }
422
423 switch (item_type_byte) {
424 case 0x10: // Application Context
425 application_context = read_uid(data, pos, item_length);
426 break;
427
428 case 0x20: // Presentation Context (RQ)
429 if (is_rq) {
430 // Presentation Context RQ item body is:
431 // context_id (1) + reserved (3) + sub-items
432 // Require at least the 4-byte fixed header before reading.
433 if (item_length < 4) {
434 return make_error<std::tuple<std::string,
435 std::vector<presentation_context_rq>,
436 std::vector<presentation_context_ac>,
438 "Presentation Context RQ item too small");
439 }
440
442 pc.id = data[pos];
443 // 3 reserved bytes
444
445 size_t sub_pos = pos + 4;
446 const size_t pc_end = pos + item_length;
447
448 while (sub_pos < pc_end) {
449 if (sub_pos + 4 > pc_end) break;
450
451 const uint8_t sub_type = data[sub_pos];
452 const uint16_t sub_length = read_uint16_be(data, sub_pos + 2);
453 sub_pos += 4;
454
455 if (sub_pos + sub_length > pc_end) break;
456
457 if (sub_type == 0x30) { // Abstract Syntax
458 pc.abstract_syntax = read_uid(data, sub_pos, sub_length);
459 } else if (sub_type == 0x40) { // Transfer Syntax
460 pc.transfer_syntaxes.push_back(
461 read_uid(data, sub_pos, sub_length));
462 }
463 sub_pos += sub_length;
464 }
465 pcs_rq.push_back(std::move(pc));
466 }
467 break;
468
469 case 0x21: // Presentation Context (AC)
470 if (!is_rq) {
471 // Presentation Context AC item body is:
472 // context_id (1) + reserved (1) + result (1) + reserved (1)
473 // + sub-items
474 // Require at least the 4-byte fixed header before reading.
475 if (item_length < 4) {
476 return make_error<std::tuple<std::string,
477 std::vector<presentation_context_rq>,
478 std::vector<presentation_context_ac>,
480 "Presentation Context AC item too small");
481 }
482
484 pc.id = data[pos];
485 // Reserved byte at pos + 1
486 pc.result = static_cast<presentation_context_result>(data[pos + 2]);
487 // Reserved byte at pos + 3
488
489 size_t sub_pos = pos + 4;
490 const size_t pc_end = pos + item_length;
491
492 while (sub_pos < pc_end) {
493 if (sub_pos + 4 > pc_end) break;
494
495 const uint8_t sub_type = data[sub_pos];
496 const uint16_t sub_length = read_uint16_be(data, sub_pos + 2);
497 sub_pos += 4;
498
499 if (sub_pos + sub_length > pc_end) break;
500
501 if (sub_type == 0x40) { // Transfer Syntax
502 pc.transfer_syntax = read_uid(data, sub_pos, sub_length);
503 }
504 sub_pos += sub_length;
505 }
506 pcs_ac.push_back(std::move(pc));
507 }
508 break;
509
510 case 0x50: { // User Information
511 auto ui_result = decode_user_info_item(
512 data.subspan(pos, item_length));
513 if (ui_result.is_ok()) {
514 user_info = std::move(ui_result.value());
515 }
516 break;
517 }
518
519 default:
520 // Skip unknown item types
521 break;
522 }
523
524 pos += item_length;
525 }
526
527 return make_ok(std::make_tuple(
528 std::move(application_context),
529 std::move(pcs_rq),
530 std::move(pcs_ac),
531 std::move(user_info)));
532}
static DecodeResult< user_information > decode_user_info_item(std::span< const uint8_t > data)
Decode User Information sub-items.
@ presentation_context_rq
Presentation Context Item (RQ)
@ presentation_context_ac
Presentation Context Item (AC)
@ application_context
Application Context Item.
presentation_context_result
Result values for A-ASSOCIATE-AC presentation context.
Definition pdu_types.h:59

References kcenon::pacs::network::presentation_context_rq::abstract_syntax, kcenon::pacs::network::application_context, kcenon::pacs::network::buffer_overflow, decode_user_info_item(), kcenon::pacs::network::presentation_context_ac::id, kcenon::pacs::network::presentation_context_rq::id, kcenon::pacs::network::malformed_pdu, read_uid(), read_uint16_be(), kcenon::pacs::network::presentation_context_ac::result, kcenon::pacs::network::presentation_context_ac::transfer_syntax, and kcenon::pacs::network::presentation_context_rq::transfer_syntaxes.

Referenced by decode_associate_ac(), and decode_associate_rq().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ pdu_length()

std::optional< size_t > kcenon::pacs::network::pdu_decoder::pdu_length ( std::span< const uint8_t > data)
staticnodiscard

Check if a complete PDU is available in the buffer.

Parameters
dataInput byte buffer
Returns
PDU length if complete PDU available, std::nullopt otherwise

Returns the total PDU length (header + data) if at least one complete PDU is present in the buffer. Useful for streaming protocols where data arrives in chunks.

Definition at line 146 of file pdu_decoder.cpp.

146 {
147 if (data.size() < PDU_HEADER_SIZE) {
148 return std::nullopt;
149 }
150
151 const uint32_t length = read_uint32_be(data, 2);
152 const size_t total = PDU_HEADER_SIZE + length;
153
154 if (data.size() < total) {
155 return std::nullopt;
156 }
157
158 return total;
159}
@ length
Linear distance measurement.

References read_uint32_be().

Referenced by decode_associate_ac(), decode_associate_rq(), decode_p_data_tf(), kcenon::pacs::network::v2::dicom_association_handler::process_buffer(), and validate_pdu_header().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ peek_pdu_type()

std::optional< pdu_type > kcenon::pacs::network::pdu_decoder::peek_pdu_type ( std::span< const uint8_t > data)
staticnodiscard

Get the PDU type from buffer without full decoding.

Parameters
dataInput byte buffer (must have at least 1 byte)
Returns
PDU type if valid, std::nullopt if invalid or insufficient data

Definition at line 161 of file pdu_decoder.cpp.

161 {
162 if (data.empty()) {
163 return std::nullopt;
164 }
165
166 switch (data[0]) {
167 case 0x01: return pdu_type::associate_rq;
168 case 0x02: return pdu_type::associate_ac;
169 case 0x03: return pdu_type::associate_rj;
170 case 0x04: return pdu_type::p_data_tf;
171 case 0x05: return pdu_type::release_rq;
172 case 0x06: return pdu_type::release_rp;
173 case 0x07: return pdu_type::abort;
174 default: return std::nullopt;
175 }
176}
@ p_data_tf
P-DATA-TF (Data Transfer)
@ release_rq
A-RELEASE-RQ (Release Request)
@ release_rp
A-RELEASE-RP (Release Response)

References kcenon::pacs::network::abort, kcenon::pacs::network::associate_ac, kcenon::pacs::network::associate_rj, kcenon::pacs::network::associate_rq, kcenon::pacs::network::p_data_tf, kcenon::pacs::network::release_rp, and kcenon::pacs::network::release_rq.

Referenced by kcenon::pacs::network::v2::dicom_association_handler::process_buffer().

Here is the caller graph for this function:

◆ read_ae_title()

std::string kcenon::pacs::network::pdu_decoder::read_ae_title ( std::span< const uint8_t > data,
size_t offset )
staticnodiscardprivate

Read an AE Title (16 bytes, space-trimmed).

Definition at line 79 of file pdu_decoder.cpp.

79 {
80 if (offset + AE_TITLE_LENGTH > data.size()) {
81 return {};
82 }
83 std::string ae_title(reinterpret_cast<const char*>(data.data() + offset),
85 // Trim trailing spaces
86 auto end = ae_title.find_last_not_of(' ');
87 if (end != std::string::npos) {
88 ae_title.resize(end + 1);
89 } else if (ae_title.find_first_not_of(' ') == std::string::npos) {
90 ae_title.clear(); // All spaces
91 }
92 return ae_title;
93}
constexpr size_t AE_TITLE_LENGTH
AE Title length (fixed 16 characters, space-padded)
Definition pdu_types.h:273

References kcenon::pacs::network::AE_TITLE_LENGTH.

Referenced by decode_associate_ac(), and decode_associate_rq().

Here is the caller graph for this function:

◆ read_uid()

std::string kcenon::pacs::network::pdu_decoder::read_uid ( std::span< const uint8_t > data,
size_t offset,
size_t length )
staticnodiscardprivate

Read a UID string (trim trailing null/space padding).

Definition at line 95 of file pdu_decoder.cpp.

96 {
97 if (offset + length > data.size()) {
98 return {};
99 }
100 std::string uid(reinterpret_cast<const char*>(data.data() + offset), length);
101 // Trim trailing nulls and spaces
102 while (!uid.empty() && (uid.back() == '\0' || uid.back() == ' ')) {
103 uid.pop_back();
104 }
105 return uid;
106}
std::string_view uid

References uid.

Referenced by decode_user_info_item(), and decode_variable_items().

Here is the caller graph for this function:

◆ read_uint16_be()

uint16_t kcenon::pacs::network::pdu_decoder::read_uint16_be ( std::span< const uint8_t > data,
size_t offset )
staticnodiscardprivate

Read a 16-bit unsigned integer in big-endian format.

Definition at line 66 of file pdu_decoder.cpp.

66 {
67 return static_cast<uint16_t>(
68 (static_cast<uint16_t>(data[offset]) << 8) |
69 static_cast<uint16_t>(data[offset + 1]));
70}

Referenced by decode_associate_ac(), decode_associate_rq(), decode_user_info_item(), and decode_variable_items().

Here is the caller graph for this function:

◆ read_uint32_be()

uint32_t kcenon::pacs::network::pdu_decoder::read_uint32_be ( std::span< const uint8_t > data,
size_t offset )
staticnodiscardprivate

Read a 32-bit unsigned integer in big-endian format.

Definition at line 72 of file pdu_decoder.cpp.

72 {
73 return (static_cast<uint32_t>(data[offset]) << 24) |
74 (static_cast<uint32_t>(data[offset + 1]) << 16) |
75 (static_cast<uint32_t>(data[offset + 2]) << 8) |
76 static_cast<uint32_t>(data[offset + 3]);
77}

Referenced by decode_p_data_tf(), decode_user_info_item(), pdu_length(), and validate_pdu_header().

Here is the caller graph for this function:

◆ validate_pdu_header()

DecodeResult< uint32_t > kcenon::pacs::network::pdu_decoder::validate_pdu_header ( std::span< const uint8_t > data,
uint8_t expected_type = 0 )
staticnodiscardprivate

Validate PDU header and extract length.

Parameters
dataInput data
expected_typeExpected PDU type (0 for any)
Returns
Pair of (is_valid, pdu_length) or error

Definition at line 108 of file pdu_decoder.cpp.

109 {
110
111 if (data.size() < PDU_HEADER_SIZE) {
112 return make_error<uint32_t>(pdu_decode_error::incomplete_header);
113 }
114
115 if (expected_type != 0 && data[0] != expected_type) {
116 return make_error<uint32_t>(pdu_decode_error::invalid_pdu_type,
117 "Expected type " + std::to_string(expected_type) +
118 ", got " + std::to_string(data[0]));
119 }
120
121 const uint32_t pdu_length = read_uint32_be(data, 2);
122
123 // Reject unreasonably large PDUs to prevent memory exhaustion attacks
124 constexpr uint32_t MAX_PDU_LENGTH = 16 * 1024 * 1024; // 16 MB
125 if (pdu_length > MAX_PDU_LENGTH) {
126 return make_error<uint32_t>(pdu_decode_error::malformed_pdu,
127 "PDU length " + std::to_string(pdu_length) +
128 " exceeds maximum allowed " + std::to_string(MAX_PDU_LENGTH));
129 }
130
131 const size_t total_length = PDU_HEADER_SIZE + pdu_length;
132
133 if (data.size() < total_length) {
134 return make_error<uint32_t>(pdu_decode_error::incomplete_pdu,
135 "Need " + std::to_string(total_length) +
136 " bytes, have " + std::to_string(data.size()));
137 }
138
139 return make_ok(pdu_length);
140}

References kcenon::pacs::network::incomplete_header, kcenon::pacs::network::incomplete_pdu, kcenon::pacs::network::invalid_pdu_type, kcenon::pacs::network::malformed_pdu, pdu_length(), and read_uint32_be().

Referenced by decode_abort(), decode_associate_ac(), decode_associate_rj(), decode_associate_rq(), decode_p_data_tf(), decode_release_rp(), and decode_release_rq().

Here is the call graph for this function:
Here is the caller graph for this function:

The documentation for this class was generated from the following files: