/* packet-dcm.c * Routines for DICOM dissection * Copyright 2003, Rich Coe * Copyright 2008-2019, David Aggeler * * DICOM communication protocol: https://www.dicomstandard.org/current/ * * Part 5: Data Structures and Encoding * Part 6: Data Dictionary * Part 7: Message Exchange * Part 8: Network Communication Support for Message Exchange * Part 10: Media Storage and File Format * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ /* * * ToDo * * - Implement value multiplicity (VM) consistently in dissect_dcm_tag_value() * - Syntax detection, in case an association request is missing in capture * - Read private tags from configuration and parse in capture * * History * * Feb 2019 - David Aggeler * * - Fixed re-assembly and export (consolidated duplicate code) * - Fixed random COL_INFO issues * - Improved COL_INFO for C-FIND * - Improved COL_INFO for multiple PDUs in one frame * * Feb 2019 - Rickard Holmberg * * - Updated DICOM definitions to 2019a * * Oct 2018 - Rickard Holmberg * * - Moved DICOM definitions to packet-dcm.h * - Generate definitions from docbook with Phyton script * - Updated DICOM definitions to 2018e * * June 2018 - David Aggeler * * - Fixed initial COL_INFO for associations. It used to 'append' instead of 'set'. * - Changed initial length check from tvb_reported_length() to tvb_captured_length() * - Heuristic Dissection: * o Modified registration, so it can be clearly identified in the Enable/Disable Protocols dialog * o Enabled by default * o Return proper data type * * February 2018 - David Aggeler * * - Fixed Bug 14415. Some tag descriptions which are added to the parent item (32 tags). * If one of those was empty a crash occurred. Mainly the RTPlan modality was affected. * - Fixed length decoding for OD, OL, UC, UR * - Fixed hf_dcm_assoc_item_type to be interpreted as 1 byte * - Fixed pdu_type to be interpreted as 1 byte * - Fixed decoding of AT type, where value length was wrongly reported in capture as 2 (instead of n*4) * * Misc. authors & dates * * - Fixed 'AT' value representation. The 'element' was equal to the 'group'. * - Changed 'FL' value representations * * September 2013 - Pascal Quantin * * - Replace all ep_ and se_ allocation with wmem_ allocations * * February 2013 - Stefan Allers * * - Support for dissection of Extended Negotiation (Query/Retrieve) * - Support for dissection of SCP/SCU Role Selection * - Support for dissection of Async Operations Window Negotiation * - Fixed: Improper calculation of length for Association Header * - Missing UIDs (Transfer Syntax, SOP Class...) added acc. PS 3.x-2011 * * Jul 11, 2010 - David Aggeler * * - Finally, better reassembly using fragment_add_seq_next(). * The previous mode is still supported. * - Fixed sporadic decoding and export issues. Always decode * association negotiation, since performance check (tree==NULL) * is now only in dissect_dcm_pdv_fragmented(). * - Added one more PDV length check * - Show Association Headers as individual items * - Code cleanup. i.e. moved a few lookup functions to be closer to the dissection * * May 13, 2010 - David Aggeler (SVN 32815) * * - Fixed HF to separate signed & unsigned values and to have BASE_DEC all signed ones * - Fixed private sequences with undefined length in ILE * - Fixed some spellings in comments * * May 27, 2009 - David Aggeler (SVN 29060) * * - Fixed corrupt files on DICOM Export * - Fixed memory limitation on DICOM Export * - Removed minimum packet length for static port mode * - Simplified checks for heuristic mode * - Removed unused functions * * May 17, 2009 - David Aggeler (SVN 28392) * * - Spelling * - Added expert_add_info() for status responses with warning & error level * - Added command details in info column (optionally) * * Dec 19, 2008 to Mar 29, 2009 - Misc (SVN 27880) * * - Spellings, see SVN * * Oct 26, 2008 - David Aggeler (SVN 26662) * * - Support remaining DICOM/ARCNEMA tags * * Oct 3, 2008 - David Aggeler (SVN 26417) * * - DICOM Tags: Support all tags, except for group 1000, 7Fxx * and tags (0020,3100 to 0020, 31FF). * Luckily these ones are retired anyhow * - DICOM Tags: Optionally show sequences, items and tags as subtree * - DICOM Tags: Certain items do have a summary of a few contained tags * - DICOM Tags: Support all defined VR representations * - DICOM Tags: For Explicit Syntax, use VR in the capture * - DICOM Tags: Lookup UIDs * - DICOM Tags: Handle split at PDV start and end. RT Structures were affected by this. * - DICOM Tags: Handle split in tag header * * - Added all status messages from PS 3.4 & PS 3.7 * - Fixed two more type warnings on solaris, i.e. (char *)tvb_get_ephemeral_string * - Replaced all ep_alloc() with ep_alloc0() and se_alloc() with se_alloc0() * - Replaced g_strdup with ep_strdup() or se_strdup() * - Show multiple PDU description in COL_INFO, not just last one. Still not all, but more * sophisticated logic for this column is probably overkill * - Since DICOM is a 32 bit protocol with all length items specified unsigned * all offset & position variables are now declared as uint32_t for dissect_dcm_pdu and * its nested functions. dissect_dcm_main() remained by purpose on int, * since we request data consolidation, requiring a true as return value * - Decode DVTk streams when using defined ports (not in heuristic mode) * - Changed to warning level 4 (for MSVC) and fixed the warnings * - Code cleanup & removed last DISSECTOR_ASSERT() * * Jul 25, 2008 - David Aggeler (SVN 25834) * * - Replaced unsigned char with char, since it caused a lot of warnings on solaris. * - Moved a little more form the include to this one to be consistent * * Jul 17, 2008 - David Aggeler * * - Export objects as part 10 compliant DICOM file. Finally, this major milestone has been reached. * - PDVs are now a child of the PCTX rather than the ASSOC object. * - Fixed PDV continuation for unknown tags (e.g. RT Structure Set) * - Replaced proprietary trim() with g_strstrip() * - Fixed strings that are displayed with /000 (padding of odd length) * - Added expert_add_info() for invalid flags and presentation context IDs * * Jun 17, 2008 - David Aggeler * * - Support multiple PDVs per PDU * - Better summary, in PDV, PDU header and in INFO Column, e.g. show commands like C-STORE * - Fixed Association Reject (was working before my changes) * - Fixed PDV Continuation with very small packets. Reduced minimum packet length * from 10 to 2 Bytes for PDU Type 4 * - Fixed PDV Continuation. Last packet was not found correctly. * - Fixed compilation warning (build 56 on solaris) * - Fixed tree expansion (hf_dcm_xxx) * - Added expert_add_info() for Association Reject * - Added expert_add_info() for Association Abort * - Added expert_add_info() for short PDVs (i.e. last fragment, but PDV is not completed yet) * - Clarified and grouped data structures and its related code (dcmItem, dcmState) to have * consistent _new() & _get() functions and to be according to coding conventions * - Added more function declaration to be more consistent * - All dissect_dcm_xx now have (almost) the same parameter order * - Removed DISSECTOR_ASSERT() for packet data errors. Not designed to handle this. * - Handle multiple DICOM Associations in a capture correctly, i.e. if presentation contexts are different. * * May 23, 2008 - David Aggeler * * - Added Class UID lookup, both in the association and in the transfer * - Better hierarchy for items in Association request/response and therefore better overview * This was a major rework. Abstract Syntax & Transfer Syntax are now children * of a presentation context and therefore grouped. User Info is now grouped. * - Re-assemble PDVs that span multiple PDUs, i.e fix continuation packets * This caused significant changes to the data structures * - Added preference with DICOM TCP ports, to prevent 'stealing' the conversation * i.e. don't just rely on heuristic * - Use pinfo->desegment_len instead of tcp_dissect_pdus() * - Returns number of bytes parsed * - For non DICOM packets, do not allocate any memory anymore, * - Added one DISSECTOR_ASSERT() to prevent loop with len==0. More to come * - Heuristic search is optional to save resources for non DICOM users * * - Output naming closer to DICOM Standard * - Variable names closer to Standard * - Protocol in now called DICOM not dcm anymore. * - Fixed type of a few variables to unsigned char instead of uint8_t * - Changed some of the length displays to decimal, because the hex value can * already be seen in the packet and decimal is easier for length calculation * in respect to TCP * * Apr 28, 2005 - Rich Coe * * - fix memory leak when Assoc packet is processed repeatedly in wireshark * - removed unused partial packet flag * - added better support for DICOM VR * - sequences * - report actual VR in packet display, if supplied by xfer syntax * - show that we are not displaying entire tag string with '[...]', * some tags can hold up to 2^32-1 chars * * - remove my goofy attempt at trying to get access to the fragmented packets * - process all the data in the Assoc packet even if display is off * - limit display of data in Assoc packet to defined size of the data even * if reported size is larger * - show the last tag in a packet as [incomplete] if we don't have all the data * - added framework for reporting DICOM async negotiation (not finished) * (I'm not aware of an implementation which currently supports this) * * Nov 9, 2004 - Rich Coe * * - Fixed the heuristic code -- sometimes a conversation already exists * - Fixed the dissect code to display all the tags in the PDU * * Initial - Rich Coe * * - It currently displays most of the DICOM packets. * - I've used it to debug Query/Retrieve, Storage, and Echo protocols. * - Not all DICOM tags are currently displayed symbolically. * Unknown tags are displayed as '(unknown)' * More known tags might be added in the future. * If the tag data contains a string, it will be displayed. * Even if the tag contains Explicit VR, it is not currently used to * symbolically display the data. * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "packet-tcp.h" #include "packet-dcm.h" void proto_register_dcm(void); void proto_reg_handoff_dcm(void); #define DICOM_DEFAULT_RANGE "104" /* Many thanks to http://medicalconnections.co.uk/ for the GUID */ #define WIRESHARK_IMPLEMENTATION_UID "1.2.826.0.1.3680043.8.427.10" #define WIRESHARK_MEDIA_STORAGE_SOP_CLASS_UID "1.2.826.0.1.3680043.8.427.11.1" #define WIRESHARK_MEDIA_STORAGE_SOP_INSTANCE_UID_PREFIX "1.2.826.0.1.3680043.8.427.11.2" #define WIRESHARK_IMPLEMENTATION_VERSION "WIRESHARK" static bool global_dcm_export_header = true; static unsigned global_dcm_export_minsize = 4096; /* Filter small objects in export */ static bool global_dcm_seq_subtree = true; static bool global_dcm_tag_subtree; /* Only useful for debugging */ static bool global_dcm_cmd_details = true; /* Show details in header and info column */ static bool global_dcm_reassemble = true; /* Merge fragmented PDVs */ static wmem_map_t *dcm_tag_table; static wmem_map_t *dcm_uid_table; static wmem_map_t *dcm_status_table; /* Initialize the protocol and registered fields */ static int proto_dcm; static int dicom_eo_tap; static int hf_dcm_pdu_type; static int hf_dcm_pdu_len; static int hf_dcm_assoc_version; static int hf_dcm_assoc_called; static int hf_dcm_assoc_calling; static int hf_dcm_assoc_reject_result; static int hf_dcm_assoc_reject_source; static int hf_dcm_assoc_reject_reason; static int hf_dcm_assoc_abort_source; static int hf_dcm_assoc_abort_reason; static int hf_dcm_assoc_item_type; static int hf_dcm_assoc_item_len; static int hf_dcm_actx; static int hf_dcm_pctx_id; static int hf_dcm_pctx_result; static int hf_dcm_pctx_abss_syntax; static int hf_dcm_pctx_xfer_syntax; static int hf_dcm_info; static int hf_dcm_info_uid; static int hf_dcm_info_version; static int hf_dcm_info_extneg; static int hf_dcm_info_extneg_sopclassuid_len; static int hf_dcm_info_extneg_sopclassuid; static int hf_dcm_info_extneg_relational_query; static int hf_dcm_info_extneg_date_time_matching; static int hf_dcm_info_extneg_fuzzy_semantic_matching; static int hf_dcm_info_extneg_timezone_query_adjustment; static int hf_dcm_info_rolesel; static int hf_dcm_info_rolesel_sopclassuid_len; static int hf_dcm_info_rolesel_sopclassuid; static int hf_dcm_info_rolesel_scurole; static int hf_dcm_info_rolesel_scprole; static int hf_dcm_info_async_neg; static int hf_dcm_info_async_neg_max_num_ops_inv; static int hf_dcm_info_async_neg_max_num_ops_per; static int hf_dcm_info_user_identify; static int hf_dcm_info_user_identify_type; static int hf_dcm_info_user_identify_response_requested; static int hf_dcm_info_user_identify_primary_field_length; static int hf_dcm_info_user_identify_primary_field; static int hf_dcm_info_user_identify_secondary_field_length; static int hf_dcm_info_user_identify_secondary_field; static int hf_dcm_info_unknown; static int hf_dcm_assoc_item_data; static int hf_dcm_pdu_maxlen; static int hf_dcm_pdv_len; static int hf_dcm_pdv_ctx; static int hf_dcm_pdv_flags; static int hf_dcm_data_tag; static int hf_dcm_tag; static int hf_dcm_tag_vr; static int hf_dcm_tag_vl; static int hf_dcm_tag_value_str; static int hf_dcm_tag_value_16u; static int hf_dcm_tag_value_16s; static int hf_dcm_tag_value_32s; static int hf_dcm_tag_value_32u; static int hf_dcm_tag_value_byte; /* Initialize the subtree pointers */ static int ett_dcm; static int ett_assoc; static int ett_assoc_header; static int ett_assoc_actx; static int ett_assoc_pctx; static int ett_assoc_pctx_abss; static int ett_assoc_pctx_xfer; static int ett_assoc_info; static int ett_assoc_info_uid; static int ett_assoc_info_version; static int ett_assoc_info_extneg; static int ett_assoc_info_rolesel; static int ett_assoc_info_async_neg; static int ett_assoc_info_user_identify; static int ett_assoc_info_unknown; static int ett_dcm_data; static int ett_dcm_data_pdv; static int ett_dcm_data_tag; static int ett_dcm_data_seq; static int ett_dcm_data_item; static expert_field ei_dcm_data_tag; static expert_field ei_dcm_multiple_transfer_syntax; static expert_field ei_dcm_pdv_len; static expert_field ei_dcm_pdv_flags; static expert_field ei_dcm_pdv_ctx; static expert_field ei_dcm_no_abstract_syntax; static expert_field ei_dcm_no_abstract_syntax_uid; static expert_field ei_dcm_status_msg; static expert_field ei_dcm_no_transfer_syntax; static expert_field ei_dcm_multiple_abstract_syntax; static expert_field ei_dcm_invalid_pdu_length; static expert_field ei_dcm_assoc_item_len; static expert_field ei_dcm_assoc_rejected; static expert_field ei_dcm_assoc_aborted; static dissector_handle_t dcm_handle; static const value_string dcm_pdu_ids[] = { { 1, "ASSOC Request" }, { 2, "ASSOC Accept" }, { 3, "ASSOC Reject" }, { 4, "Data" }, { 5, "RELEASE Request" }, { 6, "RELEASE Response" }, { 7, "ABORT" }, { 0, NULL } }; static const value_string dcm_assoc_item_type[] = { { 0x10, "Application Context" }, { 0x20, "Presentation Context" }, { 0x21, "Presentation Context Reply" }, { 0x30, "Abstract Syntax" }, { 0x40, "Transfer Syntax" }, { 0x50, "User Info" }, { 0x51, "Max Length" }, { 0x52, "Implementation Class UID" }, { 0x53, "Asynchronous Operations Window Negotiation" }, { 0x54, "SCP/SCU Role Selection" }, { 0x55, "Implementation Version" }, { 0x56, "SOP Class Extended Negotiation" }, { 0x58, "User Identity" }, { 0, NULL } }; static const value_string user_identify_type_vals[] = { { 1, "Username as a string in UTF-8" }, { 2, "Username as a string in UTF-8 and passcode" }, { 3, "Kerberos Service ticket" }, { 4, "SAML Assertion" }, { 0, NULL } }; /* Used for DICOM Export Object feature */ typedef struct _dicom_eo_t { uint32_t pkt_num; const char *hostname; const char *filename; const char *content_type; uint32_t payload_len; const uint8_t *payload_data; } dicom_eo_t; static tap_packet_status dcm_eo_packet(void *tapdata, packet_info *pinfo, epan_dissect_t *edt _U_, const void *data, tap_flags_t flags _U_) { export_object_list_t *object_list = (export_object_list_t *)tapdata; const dicom_eo_t *eo_info = (const dicom_eo_t *)data; export_object_entry_t *entry; if (eo_info) { /* We have data waiting for us */ /* The values will be freed when the export Object window is closed. Therefore, strings and buffers must be copied. */ entry = g_new(export_object_entry_t, 1); entry->pkt_num = pinfo->num; entry->hostname = g_strdup(eo_info->hostname); entry->content_type = g_strdup(eo_info->content_type); /* g_path_get_basename() allocates a new string */ entry->filename = g_path_get_basename(eo_info->filename); entry->payload_len = eo_info->payload_len; entry->payload_data = (uint8_t *)g_memdup2(eo_info->payload_data, eo_info->payload_len); object_list->add_entry(object_list->gui_data, entry); return TAP_PACKET_REDRAW; /* State changed - window should be redrawn */ } else { return TAP_PACKET_DONT_REDRAW; /* State unchanged - no window updates needed */ } } /* ************************************************************************* */ /* Fragment items */ /* ************************************************************************* */ /* Initialize the subtree pointers */ static int ett_dcm_pdv; static int ett_dcm_pdv_fragment; static int ett_dcm_pdv_fragments; static int hf_dcm_pdv_fragments; static int hf_dcm_pdv_fragment; static int hf_dcm_pdv_fragment_overlap; static int hf_dcm_pdv_fragment_overlap_conflicts; static int hf_dcm_pdv_fragment_multiple_tails; static int hf_dcm_pdv_fragment_too_long_fragment; static int hf_dcm_pdv_fragment_error; static int hf_dcm_pdv_fragment_count; static int hf_dcm_pdv_reassembled_in; static int hf_dcm_pdv_reassembled_length; static const fragment_items dcm_pdv_fragment_items = { /* Fragment subtrees */ &ett_dcm_pdv_fragment, &ett_dcm_pdv_fragments, /* Fragment fields */ &hf_dcm_pdv_fragments, &hf_dcm_pdv_fragment, &hf_dcm_pdv_fragment_overlap, &hf_dcm_pdv_fragment_overlap_conflicts, &hf_dcm_pdv_fragment_multiple_tails, &hf_dcm_pdv_fragment_too_long_fragment, &hf_dcm_pdv_fragment_error, &hf_dcm_pdv_fragment_count, &hf_dcm_pdv_reassembled_in, &hf_dcm_pdv_reassembled_length, /* Reassembled data field */ NULL, /* Tag */ "Message fragments" }; /* Structure to handle fragmented DICOM PDU packets */ static reassembly_table dcm_pdv_reassembly_table; typedef struct dcm_open_tag { /* Contains information about an open tag in a PDV, in case it was not complete. This implementation differentiates between open headers (grm, elm, vr, vl) and open values. This data structure will handle both cases. Open headers are not shown in the packet where the tag starts, but only in the next PDV. Open values are shown in the packet where the tag starts, with as the value The same PDV can close an open tag from a previous PDV at the beginning and at the same have time open a new tag at the end. The closing part at the beginning does not have its own persistent data. Do not overwrite the values, once defined, to save some memory. Since PDVs are always n*2 bytes, store each of the 2 Bytes in a variable. This way, we don't need to call tvb_get_xxx on a self created buffer */ bool is_header_fragmented; bool is_value_fragmented; uint32_t len_decoded; /* Should only be < 16 bytes */ uint16_t grp; /* Already decoded group */ uint16_t elm; /* Already decoded element */ char *vr; /* Already decoded VR */ bool is_vl_long; /* If true, Value Length is 4 Bytes, otherwise 2 */ uint16_t vl_1; /* Partially decoded 1st two bytes of length */ uint16_t vl_2; /* Partially decoded 2nd two bytes of length */ /* These ones are, where the value was truncated */ uint32_t len_total; /* Tag length of 'over-sized' tags. Used for display */ uint32_t len_remaining; /* Remaining tag bytes to 'decoded' as binary data after this PDV */ char *desc; /* Last decoded description */ } dcm_open_tag_t; /* Per Data PDV store data needed, to allow decoding of tags longer than a PDV */ typedef struct dcm_state_pdv { struct dcm_state_pdv *next, *prev; uint32_t packet_no; /* Wireshark packet number, where pdv starts */ uint32_t offset; /* Offset in packet, where PDV header starts */ char *desc; /* PDV description. wmem_file_scope() */ uint8_t pctx_id; /* Reference to used Presentation Context */ /* Following is derived from the transfer syntax in the parent PCTX, except for Command PDVs */ uint8_t syntax; /* Used and filled for Export Object only */ void *data; /* Copy of PDV data without any PDU/PDV header */ uint32_t data_len; /* Length of this PDV buffer. If >0, memory has been allocated */ char *sop_class_uid; /* SOP Class UID. Set in 1st PDV of a DICOM object. wmem_file_scope() */ char *sop_instance_uid; /* SOP Instance UID. Set in 1st PDV of a DICOM object. wmem_file_scope() */ /* End Export use */ bool is_storage; /* True, if the Data PDV is on the context of a storage SOP Class */ bool is_flagvalid; /* The following two flags are initialized correctly */ bool is_command; /* This PDV is a command rather than a data package */ bool is_last_fragment; /* Last Fragment bit was set, i.e. termination of an object This flag delimits different DICOM object in the same association */ bool is_corrupt; /* Early termination of long PDVs */ /* The following five attributes are only used for command PDVs */ char *command; /* Decoded command as text */ char *status; /* Decoded status as text */ char *comment; /* Error comment, if any */ bool is_warning; /* Command response is a cancel, warning, error */ bool is_pending; /* Command response is 'Current Match is supplied. Sub-operations are continuing' */ uint16_t message_id; /* (0000,0110) Message ID */ uint16_t message_id_resp; /* (0000,0120) Message ID being responded to */ uint16_t no_remaining; /* (0000,1020) Number of remaining sub-operations */ uint16_t no_completed; /* (0000,1021) Number of completed sub-operations */ uint16_t no_failed; /* (0000,1022) Number of failed sub-operations */ uint16_t no_warning; /* (0000,1023) Number of warning sub-operations */ dcm_open_tag_t open_tag; /* Container to store information about a fragmented tag */ uint8_t reassembly_id; } dcm_state_pdv_t; /* Per Presentation Context in an association store data needed, for subsequent decoding */ typedef struct dcm_state_pctx { struct dcm_state_pctx *next, *prev; uint8_t id; /* 0x20 Presentation Context ID */ char *abss_uid; /* 0x30 Abstract syntax */ char *abss_desc; /* 0x30 Abstract syntax decoded*/ char *xfer_uid; /* 0x40 Accepted Transfer syntax */ char *xfer_desc; /* 0x40 Accepted Transfer syntax decoded*/ uint8_t syntax; /* Decoded transfer syntax */ #define DCM_ILE 0x01 /* implicit, little endian */ #define DCM_EBE 0x02 /* explicit, big endian */ #define DCM_ELE 0x03 /* explicit, little endian */ #define DCM_UNK 0xf0 uint8_t reassembly_count; dcm_state_pdv_t *first_pdv, *last_pdv; /* List of PDV objects */ } dcm_state_pctx_t; typedef struct dcm_state_assoc { struct dcm_state_assoc *next, *prev; dcm_state_pctx_t *first_pctx, *last_pctx; /* List of Presentation context objects */ uint32_t packet_no; /* Wireshark packet number, where association starts */ char *ae_called; /* Called AE title in A-ASSOCIATE RQ */ char *ae_calling; /* Calling AE title in A-ASSOCIATE RQ */ char *ae_called_resp; /* Called AE title in A-ASSOCIATE RP */ char *ae_calling_resp; /* Calling AE title in A-ASSOCIATE RP */ } dcm_state_assoc_t; typedef struct dcm_state { struct dcm_state_assoc *first_assoc, *last_assoc; bool valid; /* this conversation is a DICOM conversation */ } dcm_state_t; /* --------------------------------------------------------------------- * DICOM Status Value Definitions * * Collected from PS 3.7 & 3.4 * */ typedef struct dcm_status { const uint16_t value; const char *description; } dcm_status_t; static dcm_status_t const dcm_status_data[] = { /* From PS 3.7 */ { 0x0000, "Success"}, { 0x0105, "No such attribute"}, { 0x0106, "Invalid attribute value"}, { 0x0107, "Attribute list error"}, { 0x0110, "Processing failure"}, { 0x0111, "Duplicate SOP instance"}, { 0x0112, "No Such object instance"}, { 0x0113, "No such event type"}, { 0x0114, "No such argument"}, { 0x0115, "Invalid argument value"}, { 0x0116, "Attribute Value Out of Range"}, { 0x0117, "Invalid object instance"}, { 0x0118, "No Such SOP class"}, { 0x0119, "Class-instance conflict"}, { 0x0120, "Missing attribute"}, { 0x0121, "Missing attribute value"}, { 0x0122, "Refused: SOP class not supported"}, { 0x0123, "No such action type"}, { 0x0210, "Duplicate invocation"}, { 0x0211, "Unrecognized operation"}, { 0x0212, "Mistyped argument"}, { 0x0213, "Resource limitation"}, { 0xFE00, "Cancel"}, /* from PS 3.4 */ { 0x0001, "Requested optional Attributes are not supported"}, { 0xA501, "Refused because General Purpose Scheduled Procedure Step Object may no longer be updated"}, { 0xA502, "Refused because the wrong Transaction UID is used"}, { 0xA503, "Refused because the General Purpose Scheduled Procedure Step SOP Instance is already in the 'IN PROGRESS' state"}, { 0xA504, "Refused because the related General Purpose Scheduled Procedure Step SOP Instance is not in the 'IN PROGRESS' state"}, { 0xA505, "Refused because Referenced General Purpose Scheduled Procedure Step Transaction UID does not match the Transaction UID of the N-ACTION request"}, { 0xA510, "Refused because an Initiate Media Creation action has already been received for this SOP Instance"}, { 0xA700, "Refused: Out of Resources"}, { 0xA701, "Refused: Out of Resources - Unable to calculate number of matches"}, { 0xA702, "Refused: Out of Resources - Unable to perform sub-operations"}, /* { 0xA7xx, "Refused: Out of Resources"}, */ { 0xA801, "Refused: Move Destination unknown"}, /* { 0xA9xx, "Error: Data Set does not match SOP Class"}, */ { 0xB000, "Sub-operations Complete - One or more Failures"}, { 0xB006, "Elements Discarded"}, { 0xB007, "Data Set does not match SOP Class"}, { 0xB101, "Specified Synchronization Frame of Reference UID does not match SCP Synchronization Frame of Reference"}, { 0xB102, "Study Instance UID coercion; Event logged under a different Study Instance UID"}, { 0xB104, "IDs inconsistent in matching a current study; Event logged"}, { 0xB605, "Requested Min Density or Max Density outside of printer's operating range. The printer will use its respective minimum or maximum density value instead"}, { 0xC000, "Error: Cannot understand/Unable to process"}, { 0xC100, "More than one match found"}, { 0xC101, "Procedural Logging not available for specified Study Instance UID"}, { 0xC102, "Event Information does not match Template"}, { 0xC103, "Cannot match event to a current study"}, { 0xC104, "IDs inconsistent in matching a current study; Event not logged"}, { 0xC200, "Unable to support requested template"}, { 0xC201, "Media creation request already completed"}, { 0xC202, "Media creation request already in progress and cannot be interrupted"}, { 0xC203, "Cancellation denied for unspecified reason"}, /* { 0xCxxx, "Error: Cannot understand/Unable to Process"}, { 0xFE00, "Matching/Sub-operations terminated due to Cancel request"}, */ { 0xFF00, "Current Match is supplied. Sub-operations are continuing"}, { 0xFF01, "Matches are continuing - Warning that one or more Optional Keys were not supported for existence for this Identifier"} }; /* following definitions are used to call dissect_dcm_assoc_item() */ #define DCM_ITEM_VALUE_TYPE_UID 1 #define DCM_ITEM_VALUE_TYPE_STRING 2 #define DCM_ITEM_VALUE_TYPE_UINT32 3 /* And from here on, only use unsigned 32 bit values. Offset is always positive number in respect to the tvb buffer start */ static uint32_t dissect_dcm_pdu (tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset); static uint32_t dissect_dcm_assoc_detail(tvbuff_t *tvb, packet_info *pinfo, proto_item *ti, dcm_state_assoc_t *assoc, uint32_t offset, uint32_t len); static uint32_t dissect_dcm_tag_value(tvbuff_t * tvb, packet_info * pinfo, proto_tree * tree, dcm_state_pdv_t * pdv, uint32_t offset, uint16_t grp, uint16_t elm, uint32_t vl, uint32_t vl_max, const char * vr, char ** tag_value); static void dcm_init(void) { unsigned i; /* Create three hash tables for quick lookups */ /* Add UID objects to hash table */ dcm_uid_table = wmem_map_new(wmem_file_scope(), wmem_str_hash, g_str_equal); for (i = 0; i < array_length(dcm_uid_data); i++) { wmem_map_insert(dcm_uid_table, (void *) dcm_uid_data[i].value, (void *) &dcm_uid_data[i]); } /* Add Tag objects to hash table */ dcm_tag_table = wmem_map_new(wmem_file_scope(), g_direct_hash, g_direct_equal); for (i = 0; i < array_length(dcm_tag_data); i++) { wmem_map_insert(dcm_tag_table, GUINT_TO_POINTER(dcm_tag_data[i].tag), (void *) &dcm_tag_data[i]); } /* Add Status Values to hash table */ dcm_status_table = wmem_map_new(wmem_file_scope(), g_direct_hash, g_direct_equal); for (i = 0; i < array_length(dcm_status_data); i++) { wmem_map_insert(dcm_status_table, GUINT_TO_POINTER((uint32_t)dcm_status_data[i].value), (void *)&dcm_status_data[i]); } } /* Get or create conversation and DICOM data structure if desired. Return new or existing DICOM structure, which is used to store context IDs and transfer syntax. Return NULL in case of the structure couldn't be created. */ static dcm_state_t * dcm_state_get(packet_info *pinfo, bool create) { conversation_t *conv; dcm_state_t *dcm_data; conv = find_or_create_conversation(pinfo); dcm_data = (dcm_state_t *)conversation_get_proto_data(conv, proto_dcm); if (dcm_data == NULL && create) { dcm_data = wmem_new0(wmem_file_scope(), dcm_state_t); conversation_add_proto_data(conv, proto_dcm, dcm_data); /* Mark it as DICOM conversation. Needed for the heuristic mode, to prevent stealing subsequent packets by other dissectors */ conversation_set_dissector(conv, dcm_handle); } return dcm_data; } static dcm_state_assoc_t * dcm_state_assoc_new(dcm_state_t *dcm_data, uint32_t packet_no) { /* Create new association object and initialize the members */ dcm_state_assoc_t *assoc; assoc = wmem_new0(wmem_file_scope(), dcm_state_assoc_t); assoc->packet_no = packet_no; /* Identifier */ /* add to the end of the list */ if (dcm_data->last_assoc) { dcm_data->last_assoc->next = assoc; assoc->prev = dcm_data->last_assoc; } else { dcm_data->first_assoc = assoc; } dcm_data->last_assoc = assoc; return assoc; } /* Find or create association object based on packet number. Return NULL, if association was not found. */ static dcm_state_assoc_t * dcm_state_assoc_get(dcm_state_t *dcm_data, uint32_t packet_no, bool create) { dcm_state_assoc_t *assoc = dcm_data->first_assoc; while (assoc) { if (assoc->next) { /* we have more associations in the same stream */ if ((assoc->packet_no <= packet_no) && (packet_no < assoc->next->packet_no)) break; } else { /* last or only associations in the same stream */ if (assoc->packet_no <= packet_no) break; } assoc = assoc->next; } if (assoc == NULL && create) { assoc = dcm_state_assoc_new(dcm_data, packet_no); } return assoc; } static dcm_state_pctx_t * dcm_state_pctx_new(dcm_state_assoc_t *assoc, uint8_t pctx_id) { /* Create new presentation context object and initialize the members */ dcm_state_pctx_t *pctx; pctx = wmem_new0(wmem_file_scope(), dcm_state_pctx_t); pctx->id = pctx_id; pctx->syntax = DCM_UNK; /* add to the end of the list list */ if (assoc->last_pctx) { assoc->last_pctx->next = pctx; pctx->prev = assoc->last_pctx; } else { assoc->first_pctx = pctx; } assoc->last_pctx = pctx; return pctx; } static dcm_state_pctx_t * dcm_state_pctx_get(dcm_state_assoc_t *assoc, uint8_t pctx_id, bool create) { /* Find or create presentation context object. Return NULL, if Context ID was not found */ dcm_state_pctx_t *pctx = assoc->first_pctx; /* static char notfound[] = "not found - click on ASSOC Request"; static dcm_state_pctx_t dunk = { NULL, NULL, false, 0, notfound, notfound, notfound, notfound, DCM_UNK }; */ while (pctx) { if (pctx->id == pctx_id) break; pctx = pctx->next; } if (pctx == NULL && create) { pctx = dcm_state_pctx_new(assoc, pctx_id); } return pctx; } /* Create new PDV object and initialize all members */ static dcm_state_pdv_t* dcm_state_pdv_new(dcm_state_pctx_t *pctx, uint32_t packet_no, uint32_t offset) { dcm_state_pdv_t *pdv; pdv = wmem_new0(wmem_file_scope(), dcm_state_pdv_t); pdv->syntax = DCM_UNK; pdv->is_last_fragment = true; /* Continuation PDVs are more tricky */ pdv->packet_no = packet_no; pdv->offset = offset; /* add to the end of the list */ if (pctx->last_pdv) { pctx->last_pdv->next = pdv; pdv->prev = pctx->last_pdv; } else { pctx->first_pdv = pdv; } pctx->last_pdv = pdv; return pdv; } static dcm_state_pdv_t* dcm_state_pdv_get(dcm_state_pctx_t *pctx, uint32_t packet_no, uint32_t offset, bool create) { /* Find or create PDV object. Return NULL, if PDV was not found, based on packet number and offset */ dcm_state_pdv_t *pdv = pctx->first_pdv; while (pdv) { if ((pdv->packet_no == packet_no) && (pdv->offset == offset)) break; pdv = pdv->next; } if (pdv == NULL && create) { pdv = dcm_state_pdv_new(pctx, packet_no, offset); } return pdv; } static dcm_state_pdv_t* dcm_state_pdv_get_obj_start(dcm_state_pdv_t *pdv_curr) { dcm_state_pdv_t *pdv_first=pdv_curr; /* Get First PDV of the DICOM Object */ while (pdv_first->prev && !pdv_first->prev->is_last_fragment) { pdv_first = pdv_first->prev; } return pdv_first; } static const value_string dcm_cmd_vals[] = { { 0x0001, "C-STORE-RQ" }, { 0x0010, "C-GET-RQ" }, { 0x0020, "C-FIND-RQ" }, { 0x0021, "C-MOVE-RQ" }, { 0x0030, "C-ECHO-RQ" }, { 0x0100, "N-EVENT-REPORT-RQ" }, { 0x0110, "N-GET-RQ" }, { 0x0120, "N-SET-RQ" }, { 0x0130, "N-ACTION-RQ" }, { 0x0140, "N-CREATE-RQ" }, { 0x0150, "N-DELETE-RQ" }, { 0x8001, "C-STORE-RSP" }, { 0x8010, "C-GET-RSP" }, { 0x8020, "C-FIND-RSP" }, { 0x8021, "C-MOVE-RSP" }, { 0x8030, "C-ECHO-RSP" }, { 0x8100, "N-EVENT-REPORT-RSP" }, { 0x8110, "N-GET-RSP" }, { 0x8120, "N-SET-RSP" }, { 0x8130, "N-ACTION-RSP" }, { 0x8140, "N-CREATE-RSP" }, { 0x8150, "N-DELETE-RSP" }, { 0x0FFF, "C-CANCEL-RQ" }, { 0, NULL } }; /* Convert the two status bytes into a text based on lookup. Classification 0x0000 : SUCCESS 0x0001 & Bxxx : WARNING 0xFE00 : CANCEL 0XFFxx : PENDING All other : FAILURE */ static const char * dcm_rsp2str(uint16_t status_value) { dcm_status_t const *status = NULL; const char *s; /* Use specific text first */ status = (dcm_status_t const *)wmem_map_lookup(dcm_status_table, GUINT_TO_POINTER((uint32_t)status_value)); if (status) { s = status->description; } else { if ((status_value & 0xFF00) == 0xA700) { /* 0xA7xx */ s = "Refused: Out of Resources"; } else if ((status_value & 0xFF00) == 0xA900) { /* 0xA9xx */ s = "Error: Data Set does not match SOP Class"; } else if ((status_value & 0xF000) == 0xC000) { /* 0xCxxx */ s = "Error: Cannot understand/Unable to Process"; } else { /* Encountered at least one case, with status_value == 0xD001 */ s = "Unknown"; } } return s; } static const char* dcm_uid_or_desc(char *dcm_uid, char *dcm_desc) { /* Return Description, UID or error */ return (dcm_desc == NULL ? (dcm_uid == NULL ? "Malformed Packet" : dcm_uid) : dcm_desc); } static void dcm_set_syntax(dcm_state_pctx_t *pctx, char *xfer_uid, const char *xfer_desc) { if ((pctx == NULL) || (xfer_uid == NULL) || (xfer_desc == NULL)) return; wmem_free(wmem_file_scope(), pctx->xfer_uid); /* free prev allocated xfer */ wmem_free(wmem_file_scope(), pctx->xfer_desc); /* free prev allocated xfer */ pctx->syntax = 0; pctx->xfer_uid = wmem_strdup(wmem_file_scope(), xfer_uid); pctx->xfer_desc = wmem_strdup(wmem_file_scope(), xfer_desc); /* this would be faster to skip the common parts, and have a FSA to * find the syntax. * Absent of coding that, this is in descending order of probability */ if (0 == strcmp(xfer_uid, "1.2.840.10008.1.2")) pctx->syntax = DCM_ILE; /* implicit little endian */ else if (0 == strcmp(xfer_uid, "1.2.840.10008.1.2.1")) pctx->syntax = DCM_ELE; /* explicit little endian */ else if (0 == strcmp(xfer_uid, "1.2.840.10008.1.2.2")) pctx->syntax = DCM_EBE; /* explicit big endian */ else if (0 == strcmp(xfer_uid, "1.2.840.113619.5.2")) pctx->syntax = DCM_ILE; /* implicit little endian, big endian pixels, GE private */ else if (0 == strcmp(xfer_uid, "1.2.840.10008.1.2.4.70")) pctx->syntax = DCM_ELE; /* explicit little endian, jpeg */ else if (0 == strncmp(xfer_uid, "1.2.840.10008.1.2.4", 18)) pctx->syntax = DCM_ELE; /* explicit little endian, jpeg */ else if (0 == strcmp(xfer_uid, "1.2.840.10008.1.2.1.99")) pctx->syntax = DCM_ELE; /* explicit little endian, deflated */ } static void dcm_uint16_to_le(uint8_t *buffer, uint16_t value) { buffer[0]=(uint8_t) (value & 0x00FF); buffer[1]=(uint8_t)((value & 0xFF00) >> 8); } static void dcm_uint32_to_le(uint8_t *buffer, uint32_t value) { buffer[0]=(uint8_t) (value & 0x000000FF); buffer[1]=(uint8_t)((value & 0x0000FF00) >> 8); buffer[2]=(uint8_t)((value & 0x00FF0000) >> 16); buffer[3]=(uint8_t)((value & 0xFF000000) >> 24); } static uint32_t dcm_export_create_tag_base(uint8_t *buffer, uint32_t bufflen, uint32_t offset, uint16_t grp, uint16_t elm, uint16_t vr, const uint8_t *value_buffer, uint32_t value_len) { /* Only Explicit Little Endian is needed to create Metafile Header Generic function to write a TAG, VR, LEN & VALUE to a combined buffer The value (buffer, len) must be preprocessed by a VR specific function */ if (offset + 6 > bufflen) return bufflen; dcm_uint16_to_le(buffer + offset, grp); offset += 2; dcm_uint16_to_le(buffer + offset, elm); offset += 2; memmove(buffer + offset, dcm_tag_vr_lookup[vr], 2); offset += 2; switch (vr) { case DCM_VR_OB: case DCM_VR_OD: case DCM_VR_OF: case DCM_VR_OL: case DCM_VR_OW: case DCM_VR_SQ: case DCM_VR_UC: case DCM_VR_UR: case DCM_VR_UT: case DCM_VR_UN: /* DICOM likes it complicated. Special handling for these types */ if (offset + 6 > bufflen) return bufflen; /* Add two reserved 0x00 bytes */ dcm_uint16_to_le(buffer + offset, 0); offset += 2; /* Length is a 4 byte field */ dcm_uint32_to_le(buffer + offset, value_len); offset += 4; break; default: /* Length is a 2 byte field */ if (offset + 2 > bufflen) return bufflen; dcm_uint16_to_le(buffer + offset, (uint16_t)value_len); offset += 2; } if (offset + value_len > bufflen) return bufflen; memmove(buffer + offset, value_buffer, value_len); offset += value_len; return offset; } static uint32_t dcm_export_create_tag_uint16(uint8_t *buffer, uint32_t bufflen, uint32_t offset, uint16_t grp, uint16_t elm, uint16_t vr, uint16_t value) { return dcm_export_create_tag_base(buffer, bufflen, offset, grp, elm, vr, (uint8_t*)&value, 2); } static uint32_t dcm_export_create_tag_uint32(uint8_t *buffer, uint32_t bufflen, uint32_t offset, uint16_t grp, uint16_t elm, uint16_t vr, uint32_t value) { return dcm_export_create_tag_base(buffer, bufflen, offset, grp, elm, vr, (uint8_t*)&value, 4); } static uint32_t dcm_export_create_tag_str(uint8_t *buffer, uint32_t bufflen, uint32_t offset, uint16_t grp, uint16_t elm, uint16_t vr, const char *value) { uint32_t len; if (!value) { /* NULL object. E.g. happens if UID was not found/set. Don't create element*/ return offset; } len=(int)strlen(value); if ((len & 0x01) == 1) { /* Odd length: since buffer is 0 initialized, pad with a 0x00 */ len += 1; } return dcm_export_create_tag_base(buffer, bufflen, offset, grp, elm, vr, (const uint8_t *)value, len); } static uint8_t* dcm_export_create_header(packet_info *pinfo, uint32_t *dcm_header_len, const char *sop_class_uid, char *sop_instance_uid, char *xfer_uid) { uint8_t *dcm_header=NULL; uint32_t offset=0; uint32_t offset_header_len=0; #define DCM_HEADER_MAX 512 dcm_header=(uint8_t *)wmem_alloc0(pinfo->pool, DCM_HEADER_MAX); /* Slightly longer than needed */ /* The subsequent functions rely on a 0 initialized buffer */ offset=128; memmove(dcm_header+offset, "DICM", 4); offset+=4; offset_header_len=offset; /* remember for later */ offset+=12; /* (0002,0000) File Meta Information Group Length UL (0002,0001) File Meta Information Version OB (0002,0002) Media Storage SOP Class UID UI (0002,0003) Media Storage SOP Instance UID UI (0002,0010) Transfer Syntax UID UI (0002,0012) Implementation Class UID UI (0002,0013) Implementation Version Name SH */ offset=dcm_export_create_tag_uint16(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0001, DCM_VR_OB, 0x0100); /* will result on 00 01 since it is little endian */ offset=dcm_export_create_tag_str(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0002, DCM_VR_UI, sop_class_uid); offset=dcm_export_create_tag_str(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0003, DCM_VR_UI, sop_instance_uid); offset=dcm_export_create_tag_str(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0010, DCM_VR_UI, xfer_uid); offset=dcm_export_create_tag_str(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0012, DCM_VR_UI, WIRESHARK_IMPLEMENTATION_UID); offset=dcm_export_create_tag_str(dcm_header, DCM_HEADER_MAX, offset, 0x0002, 0x0013, DCM_VR_SH, WIRESHARK_IMPLEMENTATION_VERSION); /* Finally write the meta header length */ dcm_export_create_tag_uint32(dcm_header, DCM_HEADER_MAX, offset_header_len, 0x0002, 0x0000, DCM_VR_UL, offset-offset_header_len-12); *dcm_header_len=offset; return dcm_header; } /* Concatenate related PDVs into one buffer and add it to the export object list. Supports both modes: - Multiple DICOM PDVs are reassembled with fragment_add_seq_next() and process_reassembled_data(). In this case all data will be in the last PDV, and all its predecessors will have zero data. - DICOM PDVs are keep separate. Every PDV contains data. */ static void dcm_export_create_object(packet_info *pinfo, dcm_state_assoc_t *assoc, dcm_state_pdv_t *pdv) { dicom_eo_t *eo_info = NULL; dcm_state_pdv_t *pdv_curr = NULL; dcm_state_pdv_t *pdv_same_pkt = NULL; dcm_state_pctx_t *pctx = NULL; uint8_t *pdv_combined = NULL; uint8_t *pdv_combined_curr = NULL; uint8_t *dcm_header = NULL; uint32_t pdv_combined_len = 0; uint32_t dcm_header_len = 0; uint16_t cnt_same_pkt = 1; char *filename; const char *hostname; const char *sop_class_uid; char *sop_instance_uid; /* Calculate total PDV length, i.e. all packets until last PDV without continuation */ pdv_curr = pdv; pdv_same_pkt = pdv; pdv_combined_len=pdv_curr->data_len; while (pdv_curr->prev && !pdv_curr->prev->is_last_fragment) { pdv_curr = pdv_curr->prev; pdv_combined_len += pdv_curr->data_len; } /* Count number of PDVs with the same Packet Number */ while (pdv_same_pkt->prev && (pdv_same_pkt->prev->packet_no == pdv_same_pkt->packet_no)) { pdv_same_pkt = pdv_same_pkt->prev; cnt_same_pkt += 1; } pctx=dcm_state_pctx_get(assoc, pdv_curr->pctx_id, false); if (assoc->ae_calling != NULL && strlen(assoc->ae_calling)>0 && assoc->ae_called != NULL && strlen(assoc->ae_called)>0) { hostname = wmem_strdup_printf(pinfo->pool, "%s <-> %s", assoc->ae_calling, assoc->ae_called); } else { hostname = "AE title(s) unknown"; } if (pdv->is_storage && pdv_curr->sop_class_uid && strlen(pdv_curr->sop_class_uid)>0 && pdv_curr->sop_instance_uid && strlen(pdv_curr->sop_instance_uid)>0) { sop_class_uid = wmem_strdup(pinfo->pool, pdv_curr->sop_class_uid); sop_instance_uid = wmem_strdup(pinfo->pool, pdv_curr->sop_instance_uid); /* Make sure filename does not contain invalid character. Rather conservative. Even though this should be a valid DICOM UID, apply the same filter rules in case of bogus data. */ filename = wmem_strdup_printf(pinfo->pool, "%06d-%d-%s.dcm", pinfo->num, cnt_same_pkt, g_strcanon(pdv_curr->sop_instance_uid, G_CSET_A_2_Z G_CSET_a_2_z G_CSET_DIGITS "-.", '-')); } else { /* No SOP Instance or SOP Class UID found in PDV. Use wireshark ones */ sop_class_uid = wmem_strdup(pinfo->pool, WIRESHARK_MEDIA_STORAGE_SOP_CLASS_UID); sop_instance_uid = wmem_strdup_printf(pinfo->pool, "%s.%d.%d", WIRESHARK_MEDIA_STORAGE_SOP_INSTANCE_UID_PREFIX, pinfo->num, cnt_same_pkt); /* Make sure filename does not contain invalid character. Rather conservative.*/ filename = wmem_strdup_printf(pinfo->pool, "%06d-%d-%s.dcm", pinfo->num, cnt_same_pkt, g_strcanon(pdv->desc, G_CSET_A_2_Z G_CSET_a_2_z G_CSET_DIGITS "-.", '-')); } if (global_dcm_export_header) { if (pctx && pctx->xfer_uid && strlen(pctx->xfer_uid)>0) { dcm_header=dcm_export_create_header(pinfo, &dcm_header_len, sop_class_uid, sop_instance_uid, pctx->xfer_uid); } else { /* We are running blind, i.e. no presentation context/syntax found. Don't invent one, so the meta header will miss the transfer syntax UID tag (even though it is mandatory) */ dcm_header=dcm_export_create_header(pinfo, &dcm_header_len, sop_class_uid, sop_instance_uid, NULL); } } if (dcm_header_len + pdv_combined_len >= global_dcm_export_minsize) { /* Allocate the final size */ pdv_combined = (uint8_t *)wmem_alloc0(pinfo->pool, dcm_header_len + pdv_combined_len); pdv_combined_curr = pdv_combined; if (dcm_header_len != 0) { /* Will be 0 when global_dcm_export_header is false */ memmove(pdv_combined, dcm_header, dcm_header_len); pdv_combined_curr += dcm_header_len; } /* Copy PDV per PDV to target buffer */ while (!pdv_curr->is_last_fragment) { memmove(pdv_combined_curr, pdv_curr->data, pdv_curr->data_len); /* this is a copy not move */ pdv_combined_curr += pdv_curr->data_len; pdv_curr = pdv_curr->next; } /* Last packet */ memmove(pdv_combined_curr, pdv->data, pdv->data_len); /* this is a copy not a move */ /* Add to list */ /* The tap will copy the values and free the copies; this only * needs packet lifetime. */ eo_info = wmem_new0(pinfo->pool, dicom_eo_t); eo_info->hostname = hostname; eo_info->filename = filename; eo_info->content_type = pdv->desc; eo_info->payload_len = dcm_header_len + pdv_combined_len; eo_info->payload_data = pdv_combined; tap_queue_packet(dicom_eo_tap, pinfo, eo_info); } } /* For tags with fixed length items, calculate the value multiplicity (VM). String tags use a separator, which is not supported by this function. Support item count from 0 to n. and handles bad encoding (e.g. an 'AT' tag was reported to be 2 bytes instead of 4 bytes) */ static uint32_t dcm_vm_item_count(uint32_t value_length, uint32_t item_length) { /* This could all be formulated in a single line but it does not make it easier to read */ if (value_length == 0) { return 0; } else if (value_length <= item_length) { return 1; /* This is the special case of bad encoding */ } else { return (value_length / item_length); } } /* Decode the association header */ static uint32_t dissect_dcm_assoc_header(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset, dcm_state_assoc_t *assoc, uint8_t pdu_type, uint32_t pdu_len) { proto_item *assoc_header_pitem; proto_tree *assoc_header_ptree; /* Tree for item details */ const char *buf_desc = NULL; const char *reject_result_desc = ""; const char *reject_source_desc = ""; const char *reject_reason_desc = ""; const char *abort_source_desc = ""; const char *abort_reason_desc = ""; char *ae_called; char *ae_calling; char *ae_called_resp; char *ae_calling_resp; uint8_t reject_result; uint8_t reject_source; uint8_t reject_reason; uint8_t abort_source; uint8_t abort_reason; assoc_header_ptree = proto_tree_add_subtree(tree, tvb, offset, pdu_len, ett_assoc_header, &assoc_header_pitem, "Association Header"); switch (pdu_type) { case 1: /* Association Request */ proto_tree_add_item(assoc_header_ptree, hf_dcm_assoc_version, tvb, offset, 2, ENC_BIG_ENDIAN); offset += 2; offset += 2; /* Two reserved bytes*/ /* * XXX - this is in "the ISO 646:1990-Basic G0 Set"; ISO/IEC 646:1991 * claims to be the third edition of the standard, with the second * version being ISO 646:1983, so I'm not sure what happened to * ISO 646:1990. ISO/IEC 646:1991 speaks of "the basic 7-bit code * table", which leaves positions 2/3 (0x23) and 2/4 (0x24) as * being either NUMBER SIGN or POUND SIGN and either DOLLAR SIGN or * CURRENCY SIGN, respectively, and positions 4/0 (0x40), 5/11 (0x5b), * 5/12 (0x5c), 5/13 (0x5d), 5/14 (0x5e), 6/0 (0x60), 7/11 (0x7b), * 7/12 (0x7c), 7/13 (0x7d), and 7/14 (0x7e) as being "available for * national or application-oriented use", so I'm *guessing* that * "the ISO 646:1990-Basic G0 Set" means "those positions aren't * specified" and thus should probably be treated as not valid * in that "Basic" set. */ proto_tree_add_item_ret_display_string(assoc_header_ptree, hf_dcm_assoc_called, tvb, offset, 16, ENC_ISO_646_BASIC|ENC_NA, pinfo->pool, &ae_called); assoc->ae_called = wmem_strdup(wmem_file_scope(), g_strstrip(ae_called)); offset += 16; proto_tree_add_item_ret_display_string(assoc_header_ptree, hf_dcm_assoc_calling, tvb, offset, 16, ENC_ISO_646_BASIC|ENC_NA, pinfo->pool, &ae_calling); assoc->ae_calling = wmem_strdup(wmem_file_scope(), g_strstrip(ae_calling)); offset += 16; offset += 32; /* 32 reserved bytes */ buf_desc = wmem_strdup_printf(pinfo->pool, "A-ASSOCIATE request %s --> %s", assoc->ae_calling, assoc->ae_called); offset = dissect_dcm_assoc_detail(tvb, pinfo, assoc_header_ptree, assoc, offset, pdu_len-offset); break; case 2: /* Association Accept */ proto_tree_add_item(assoc_header_ptree, hf_dcm_assoc_version, tvb, offset, 2, ENC_BIG_ENDIAN); offset += 2; offset += 2; /* Two reserved bytes*/ proto_tree_add_item_ret_display_string(assoc_header_ptree, hf_dcm_assoc_called, tvb, offset, 16, ENC_ISO_646_BASIC|ENC_NA, pinfo->pool, &ae_called_resp); assoc->ae_called_resp = wmem_strdup(wmem_file_scope(), g_strstrip(ae_called_resp)); offset += 16; proto_tree_add_item_ret_display_string(assoc_header_ptree, hf_dcm_assoc_calling, tvb, offset, 16, ENC_ISO_646_BASIC|ENC_NA, pinfo->pool, &ae_calling_resp); assoc->ae_calling_resp = wmem_strdup(wmem_file_scope(), g_strstrip(ae_calling_resp)); offset += 16; offset += 32; /* 32 reserved bytes */ buf_desc = wmem_strdup_printf(pinfo->pool, "A-ASSOCIATE accept %s <-- %s", assoc->ae_calling_resp, assoc->ae_called_resp); offset = dissect_dcm_assoc_detail(tvb, pinfo, assoc_header_ptree, assoc, offset, pdu_len-offset); break; case 3: /* Association Reject */ offset += 1; /* One reserved byte */ reject_result = tvb_get_uint8(tvb, offset); reject_source = tvb_get_uint8(tvb, offset+1); reject_reason = tvb_get_uint8(tvb, offset+2); switch (reject_result) { case 1: reject_result_desc = "Reject Permanent"; break; case 2: reject_result_desc = "Reject Transient"; break; default: break; } switch (reject_source) { case 1: reject_source_desc = "User"; switch (reject_reason) { case 1: reject_reason_desc = "No reason given"; break; case 2: reject_reason_desc = "Application context name not supported"; break; case 3: reject_reason_desc = "Calling AE title not recognized"; break; case 7: reject_reason_desc = "Called AE title not recognized"; break; } break; case 2: reject_source_desc = "Provider (ACSE)"; switch (reject_reason) { case 1: reject_reason_desc = "No reason given"; break; case 2: reject_reason_desc = "Protocol version not supported"; break; } break; case 3: reject_source_desc = "Provider (Presentation)"; switch (reject_reason) { case 1: reject_reason_desc = "Temporary congestion"; break; case 2: reject_reason_desc = "Local limit exceeded"; break; } break; } proto_tree_add_uint_format_value(assoc_header_ptree, hf_dcm_assoc_reject_result, tvb, offset , 1, reject_result, "%s", reject_result_desc); proto_tree_add_uint_format_value(assoc_header_ptree, hf_dcm_assoc_reject_source, tvb, offset+1, 1, reject_source, "%s", reject_source_desc); proto_tree_add_uint_format_value(assoc_header_ptree, hf_dcm_assoc_reject_reason, tvb, offset+2, 1, reject_reason, "%s", reject_reason_desc); offset += 3; /* Provider aborted */ buf_desc = wmem_strdup_printf(pinfo->pool, "A-ASSOCIATE reject %s <-- %s (%s)", assoc->ae_calling, assoc->ae_called, reject_reason_desc); expert_add_info(pinfo, assoc_header_pitem, &ei_dcm_assoc_rejected); break; case 5: /* RELEASE Request */ offset += 2; /* Two reserved bytes */ buf_desc="A-RELEASE request"; break; case 6: /* RELEASE Response */ offset += 2; /* Two reserved bytes */ buf_desc="A-RELEASE response"; break; case 7: /* ABORT */ offset += 2; /* Two reserved bytes */ abort_source = tvb_get_uint8(tvb, offset); abort_reason = tvb_get_uint8(tvb, offset+1); switch (abort_source) { case 0: abort_source_desc = "User"; abort_reason_desc = "N/A"; /* No details can be provided*/ break; case 1: /* reserved */ break; case 2: abort_source_desc = "Provider"; switch (abort_reason) { case 0: abort_reason_desc = "Not specified"; break; case 1: abort_reason_desc = "Unrecognized PDU"; break; case 2: abort_reason_desc = "Unexpected PDU"; break; case 4: abort_reason_desc = "Unrecognized PDU parameter"; break; case 5: abort_reason_desc = "Unexpected PDU parameter"; break; case 6: abort_reason_desc = "Invalid PDU parameter value"; break; } break; } proto_tree_add_uint_format_value(assoc_header_ptree, hf_dcm_assoc_abort_source, tvb, offset , 1, abort_source, "%s", abort_source_desc); proto_tree_add_uint_format_value(assoc_header_ptree, hf_dcm_assoc_abort_reason, tvb, offset+1, 1, abort_reason, "%s", abort_reason_desc); offset += 2; if (abort_source == 0) { /* User aborted */ buf_desc = wmem_strdup_printf(pinfo->pool, "ABORT %s --> %s", assoc->ae_calling, assoc->ae_called); } else { /* Provider aborted, slightly more information */ buf_desc = wmem_strdup_printf(pinfo->pool, "ABORT %s <-- %s (%s)", assoc->ae_calling, assoc->ae_called, abort_reason_desc); } expert_add_info(pinfo, assoc_header_pitem, &ei_dcm_assoc_aborted); break; } if (buf_desc) { proto_item_set_text(assoc_header_pitem, "%s", buf_desc); col_set_str(pinfo->cinfo, COL_INFO, buf_desc); /* proto_item and proto_tree are one and the same */ proto_item_append_text(tree, ", %s", buf_desc); } return offset; } /* Decode one item in a association request or response. Lookup UIDs if requested. Create a subtree node with summary and three elements (item_type, item_len, value) */ static void dissect_dcm_assoc_item(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset, const char *pitem_prefix, int item_value_type, char **item_value, const char **item_description, int *hf_type, int *hf_len, int *hf_value, int ett_subtree) { proto_tree *assoc_item_ptree; /* Tree for item details */ proto_item *assoc_item_pitem; dcm_uid_t const *uid = NULL; uint32_t item_number = 0; uint8_t item_type; uint16_t item_len; char *buf_desc; /* Used for item text */ *item_value = NULL; *item_description = NULL; item_type = tvb_get_uint8(tvb, offset); item_len = tvb_get_ntohs(tvb, offset+2); assoc_item_ptree = proto_tree_add_subtree(tree, tvb, offset, item_len+4, ett_subtree, &assoc_item_pitem, pitem_prefix); proto_tree_add_uint(assoc_item_ptree, *hf_type, tvb, offset, 1, item_type); proto_tree_add_uint(assoc_item_ptree, *hf_len, tvb, offset+2, 2, item_len); switch (item_value_type) { case DCM_ITEM_VALUE_TYPE_UID: *item_value = (char *)tvb_get_string_enc(pinfo->pool, tvb, offset+4, item_len, ENC_ASCII); uid = (dcm_uid_t const *)wmem_map_lookup(dcm_uid_table, (void *) *item_value); if (uid) { *item_description = uid->name; buf_desc = wmem_strdup_printf(pinfo->pool, "%s (%s)", *item_description, *item_value); } else { /* Unknown UID, or no UID at all */ buf_desc = *item_value; } proto_item_append_text(assoc_item_pitem, "%s", buf_desc); proto_tree_add_string(assoc_item_ptree, *hf_value, tvb, offset+4, item_len, buf_desc); break; case DCM_ITEM_VALUE_TYPE_STRING: *item_value = (char *)tvb_get_string_enc(pinfo->pool, tvb, offset+4, item_len, ENC_ASCII); proto_item_append_text(assoc_item_pitem, "%s", *item_value); proto_tree_add_string(assoc_item_ptree, *hf_value, tvb, offset+4, item_len, *item_value); break; case DCM_ITEM_VALUE_TYPE_UINT32: item_number = tvb_get_ntohl(tvb, offset+4); *item_value = (char *)wmem_strdup_printf(wmem_file_scope(), "%d", item_number); proto_item_append_text(assoc_item_pitem, "%s", *item_value); proto_tree_add_item(assoc_item_ptree, *hf_value, tvb, offset+4, 4, ENC_BIG_ENDIAN); break; default: break; } } /* Decode the SOP Class Extended Negotiation Sub-Item Fields in a association request or response. Lookup UIDs if requested */ static void dissect_dcm_assoc_sopclass_extneg(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset) { proto_tree *assoc_item_extneg_tree = NULL; /* Tree for item details */ proto_item *assoc_item_extneg_item = NULL; uint16_t item_len = 0; uint16_t sop_class_uid_len = 0; int32_t cnt = 0; char *buf_desc = NULL; /* Used for item text */ dcm_uid_t const *sopclassuid=NULL; char *sopclassuid_str = NULL; item_len = tvb_get_ntohs(tvb, offset+2); sop_class_uid_len = tvb_get_ntohs(tvb, offset+4); assoc_item_extneg_item = proto_tree_add_item(tree, hf_dcm_info_extneg, tvb, offset, item_len+4, ENC_NA); proto_item_set_text(assoc_item_extneg_item, "Ext. Neg.: "); assoc_item_extneg_tree = proto_item_add_subtree(assoc_item_extneg_item, ett_assoc_info_extneg); proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_assoc_item_type, tvb, offset, 1, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_assoc_item_len, tvb, offset+2, 2, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_info_extneg_sopclassuid_len, tvb, offset+4, 2, ENC_BIG_ENDIAN); sopclassuid_str = (char *)tvb_get_string_enc(pinfo->pool, tvb, offset+6, sop_class_uid_len, ENC_ASCII); sopclassuid = (dcm_uid_t const *)wmem_map_lookup(dcm_uid_table, (void *) sopclassuid_str); if (sopclassuid) { buf_desc = wmem_strdup_printf(pinfo->pool, "%s (%s)", sopclassuid->name, sopclassuid->value); } else { buf_desc = sopclassuid_str; } proto_item_append_text(assoc_item_extneg_item, "%s", buf_desc); proto_tree_add_string(assoc_item_extneg_tree, hf_dcm_info_extneg_sopclassuid, tvb, offset+6, sop_class_uid_len, buf_desc); /* Count how many fields are following. */ cnt = item_len - 2 - sop_class_uid_len; /* * The next field contains Service Class specific information identified by the SOP Class UID. */ if (0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENT_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_FIND) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_STUDY_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_FIND) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENTSTUDY_ONLY_QUERYRETRIEVE_INFORMATION_MODEL_FIND_RETIRED) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENT_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_MOVE) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_STUDY_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_MOVE) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENTSTUDY_ONLY_QUERYRETRIEVE_INFORMATION_MODEL_MOVE_RETIRED) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENT_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_GET) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_STUDY_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_GET) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENTSTUDY_ONLY_QUERYRETRIEVE_INFORMATION_MODEL_GET_RETIRED)) { if (cnt<=0) { return; } /* Support for Relational queries. */ proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_info_extneg_relational_query, tvb, offset+6+sop_class_uid_len, 1, ENC_BIG_ENDIAN); --cnt; } /* More sub-items are only allowed for the C-FIND SOP Classes. */ if (0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENT_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_FIND) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_STUDY_ROOT_QUERYRETRIEVE_INFORMATION_MODEL_FIND) || 0 == strcmp(sopclassuid_str, DCM_UID_SOP_CLASS_PATIENTSTUDY_ONLY_QUERYRETRIEVE_INFORMATION_MODEL_FIND_RETIRED)) { if (cnt<=0) { return; } /* Combined Date-Time matching. */ proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_info_extneg_date_time_matching, tvb, offset+7+sop_class_uid_len, 1, ENC_BIG_ENDIAN); --cnt; if (cnt<=0) { return; } /* Fuzzy semantic matching of person names. */ proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_info_extneg_fuzzy_semantic_matching, tvb, offset+8+sop_class_uid_len, 1, ENC_BIG_ENDIAN); --cnt; if (cnt<=0) { return; } /* Timezone query adjustment. */ proto_tree_add_item(assoc_item_extneg_tree, hf_dcm_info_extneg_timezone_query_adjustment, tvb, offset+9+sop_class_uid_len, 1, ENC_BIG_ENDIAN); --cnt; } } /* Decode user identities in the association */ static void dissect_dcm_assoc_user_identify(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset) { proto_tree *assoc_item_user_identify_tree = NULL; /* Tree for item details */ proto_item *assoc_item_user_identify_item = NULL; uint16_t primary_field_length, secondary_field_length, item_len = 0; uint8_t type; item_len = tvb_get_ntohs(tvb, offset+2); assoc_item_user_identify_item = proto_tree_add_item(tree, hf_dcm_info_user_identify, tvb, offset, item_len+4, ENC_NA); assoc_item_user_identify_tree = proto_item_add_subtree(assoc_item_user_identify_item, ett_assoc_info_user_identify); proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_assoc_item_type, tvb, offset, 1, ENC_BIG_ENDIAN); offset += 2; proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_assoc_item_len, tvb, offset, 2, ENC_BIG_ENDIAN); offset += 2; type = tvb_get_uint8(tvb, offset); proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_type, tvb, offset, 1, ENC_BIG_ENDIAN); offset += 1; proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_response_requested, tvb, offset, 1, ENC_BIG_ENDIAN); offset += 1; primary_field_length = tvb_get_ntohs(tvb, offset); proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_primary_field_length, tvb, offset, 2, ENC_BIG_ENDIAN); offset += 2; proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_primary_field, tvb, offset, primary_field_length, ENC_UTF_8); proto_item_append_text(assoc_item_user_identify_item, ": %s", tvb_get_string_enc(pinfo->pool, tvb, offset, primary_field_length, ENC_UTF_8|ENC_NA)); offset += primary_field_length; if (type == 2) { secondary_field_length = tvb_get_ntohs(tvb, offset); proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_secondary_field_length, tvb, offset, 2, ENC_BIG_ENDIAN); offset += 2; proto_tree_add_item(assoc_item_user_identify_tree, hf_dcm_info_user_identify_secondary_field, tvb, offset, secondary_field_length, ENC_UTF_8); proto_item_append_text(assoc_item_user_identify_item, ", %s", tvb_get_string_enc(pinfo->pool, tvb, offset, secondary_field_length, ENC_UTF_8|ENC_NA)); } } /* Decode unknown item types in the association */ static void dissect_dcm_assoc_unknown(tvbuff_t *tvb, proto_tree *tree, uint32_t offset) { proto_tree *assoc_item_unknown_tree = NULL; /* Tree for item details */ proto_item *assoc_item_unknown_item = NULL; uint16_t item_len = 0; item_len = tvb_get_ntohs(tvb, offset+2); assoc_item_unknown_item = proto_tree_add_item(tree, hf_dcm_info_unknown, tvb, offset, item_len+4, ENC_NA); assoc_item_unknown_tree = proto_item_add_subtree(assoc_item_unknown_item, ett_assoc_info_unknown); proto_tree_add_item(assoc_item_unknown_tree, hf_dcm_assoc_item_type, tvb, offset, 1, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_unknown_tree, hf_dcm_assoc_item_len, tvb, offset+2, 2, ENC_BIG_ENDIAN); offset += 4; proto_tree_add_item(assoc_item_unknown_tree, hf_dcm_assoc_item_data, tvb, offset, item_len, ENC_NA); } /* Decode the SCP/SCU Role Selection Sub-Item Fields in a association request or response. Lookup UIDs if requested */ static void dissect_dcm_assoc_role_selection(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset) { proto_tree *assoc_item_rolesel_tree; /* Tree for item details */ proto_item *assoc_item_rolesel_item; uint16_t item_len, sop_class_uid_len; uint8_t scp_role, scu_role; char *buf_desc; /* Used for item text */ dcm_uid_t const *sopclassuid; char *sopclassuid_str; item_len = tvb_get_ntohs(tvb, offset+2); sop_class_uid_len = tvb_get_ntohs(tvb, offset+4); assoc_item_rolesel_item = proto_tree_add_item(tree, hf_dcm_info_rolesel, tvb, offset, item_len+4, ENC_NA); proto_item_set_text(assoc_item_rolesel_item, "Role Selection: "); assoc_item_rolesel_tree = proto_item_add_subtree(assoc_item_rolesel_item, ett_assoc_info_rolesel); proto_tree_add_item(assoc_item_rolesel_tree, hf_dcm_assoc_item_type, tvb, offset, 1, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_rolesel_tree, hf_dcm_assoc_item_len, tvb, offset+2, 2, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_rolesel_tree, hf_dcm_info_rolesel_sopclassuid_len, tvb, offset+4, 2, ENC_BIG_ENDIAN); sopclassuid_str = (char *)tvb_get_string_enc(pinfo->pool, tvb, offset+6, sop_class_uid_len, ENC_ASCII); sopclassuid = (dcm_uid_t const *)wmem_map_lookup(dcm_uid_table, (void *) sopclassuid_str); scu_role = tvb_get_uint8(tvb, offset+6+sop_class_uid_len); scp_role = tvb_get_uint8(tvb, offset+7+sop_class_uid_len); if (scu_role) { proto_item_append_text(assoc_item_rolesel_item, "%s", "SCU-role: yes"); } else { proto_item_append_text(assoc_item_rolesel_item, "%s", "SCU-role: no"); } if (scp_role) { proto_item_append_text(assoc_item_rolesel_item, ", %s", "SCP-role: yes"); } else { proto_item_append_text(assoc_item_rolesel_item, ", %s", "SCP-role: no"); } if (sopclassuid) { buf_desc = wmem_strdup_printf(pinfo->pool, "%s (%s)", sopclassuid->name, sopclassuid->value); } else { buf_desc = sopclassuid_str; } proto_tree_add_string(assoc_item_rolesel_tree, hf_dcm_info_rolesel_sopclassuid, tvb, offset+6, sop_class_uid_len, buf_desc); proto_tree_add_item(assoc_item_rolesel_tree, hf_dcm_info_rolesel_scurole, tvb, offset+6+sop_class_uid_len, 1, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_rolesel_tree, hf_dcm_info_rolesel_scprole, tvb, offset+7+sop_class_uid_len, 1, ENC_BIG_ENDIAN); } /* Decode the Asynchronous operations (and sub-operations) Window Negotiation Sub-Item Fields in a association request or response. */ static void dissect_dcm_assoc_async_negotiation(tvbuff_t *tvb, proto_tree *tree, uint32_t offset) { proto_tree *assoc_item_asyncneg_tree; /* Tree for item details */ proto_item *assoc_item_asyncneg_item; uint16_t item_len, max_num_ops_inv, max_num_ops_per = 0; item_len = tvb_get_ntohs(tvb, offset+2); assoc_item_asyncneg_item = proto_tree_add_item(tree, hf_dcm_info_async_neg, tvb, offset, item_len+4, ENC_NA); proto_item_set_text(assoc_item_asyncneg_item, "Async Negotiation: "); assoc_item_asyncneg_tree = proto_item_add_subtree(assoc_item_asyncneg_item, ett_assoc_info_async_neg); proto_tree_add_item(assoc_item_asyncneg_tree, hf_dcm_assoc_item_type, tvb, offset, 1, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_asyncneg_tree, hf_dcm_assoc_item_len, tvb, offset+2, 2, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_asyncneg_tree, hf_dcm_info_async_neg_max_num_ops_inv, tvb, offset+4, 2, ENC_BIG_ENDIAN); proto_tree_add_item(assoc_item_asyncneg_tree, hf_dcm_info_async_neg_max_num_ops_per, tvb, offset+6, 2, ENC_BIG_ENDIAN); max_num_ops_inv = tvb_get_ntohs(tvb, offset+4); max_num_ops_per = tvb_get_ntohs(tvb, offset+6); proto_item_append_text(assoc_item_asyncneg_item, "%s%d", "Maximum Number Operations Invoked: ", max_num_ops_inv); if (max_num_ops_inv==0) proto_item_append_text(assoc_item_asyncneg_item, "%s", " (unlimited)"); proto_item_append_text(assoc_item_asyncneg_item, ", %s%d", "Maximum Number Operations Performed: ", max_num_ops_per); if (max_num_ops_per==0) proto_item_append_text(assoc_item_asyncneg_item, "%s", " (unlimited)"); } /* Decode a presentation context item in a Association Request or Response. In the response, set the accepted transfer syntax, if any. */ static void dissect_dcm_pctx(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_assoc_t *assoc, uint32_t offset, uint32_t len, const char *pitem_prefix, bool is_assoc_request) { proto_tree *pctx_ptree; /* Tree for presentation context details */ proto_item *pctx_pitem; dcm_state_pctx_t *pctx = NULL; uint8_t item_type = 0; uint16_t item_len = 0; uint8_t pctx_id = 0; /* Presentation Context ID */ uint8_t pctx_result = 0; const char *pctx_result_desc = ""; char *pctx_abss_uid = NULL; /* Abstract Syntax UID alias SOP Class UID */ const char *pctx_abss_desc = NULL; /* Description of UID */ char *pctx_xfer_uid = NULL; /* Transfer Syntax UID */ const char *pctx_xfer_desc = NULL; /* Description of UID */ char *buf_desc; /* Used in info mode for item text */ uint32_t endpos = 0; int cnt_abbs = 0; /* Number of Abstract Syntax Items */ int cnt_xfer = 0; /* Number of Transfer Syntax Items */ endpos = offset + len; item_type = tvb_get_uint8(tvb, offset-4); item_len = tvb_get_ntohs(tvb, offset-2); pctx_ptree = proto_tree_add_subtree(tree, tvb, offset-4, item_len+4, ett_assoc_pctx, &pctx_pitem, pitem_prefix); pctx_id = tvb_get_uint8(tvb, offset); pctx_result = tvb_get_uint8(tvb, 2 + offset); /* only set in responses, otherwise reserved and 0x00 */ /* Find or create DICOM context object */ pctx = dcm_state_pctx_get(assoc, pctx_id, true); if (pctx == NULL) { /* Internal error. Failed to create data structure */ return; } proto_tree_add_uint(pctx_ptree, hf_dcm_assoc_item_type, tvb, offset-4, 1, item_type); /* The type is only one byte long */ proto_tree_add_uint(pctx_ptree, hf_dcm_assoc_item_len, tvb, offset-2, 2, item_len); proto_tree_add_uint_format(pctx_ptree, hf_dcm_pctx_id, tvb, offset, 1, pctx_id, "Context ID: 0x%02x", pctx_id); if (!is_assoc_request) { /* Association response. */ switch (pctx_result) { case 0: pctx_result_desc = "Accept"; break; case 1: pctx_result_desc = "User Reject"; break; case 2: pctx_result_desc = "No Reason"; break; case 3: pctx_result_desc = "Abstract Syntax Unsupported"; break; case 4: pctx_result_desc = "Transfer Syntax Unsupported"; break; } proto_tree_add_uint_format(pctx_ptree, hf_dcm_pctx_result, tvb, offset+2, 1, pctx_result, "Result: %s (0x%x)", pctx_result_desc, pctx_result); } offset += 4; while (offset < endpos) { item_type = tvb_get_uint8(tvb, offset); item_len = tvb_get_ntohs(tvb, 2 + offset); offset += 4; switch (item_type) { case 0x30: /* Abstract syntax */ /* Parse Item. Works also in info mode where dcm_pctx_tree is NULL */ dissect_dcm_assoc_item(tvb, pinfo, pctx_ptree, offset-4, "Abstract Syntax: ", DCM_ITEM_VALUE_TYPE_UID, &pctx_abss_uid, &pctx_abss_desc, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_pctx_abss_syntax, ett_assoc_pctx_abss); cnt_abbs += 1; offset += item_len; break; case 0x40: /* Transfer syntax */ dissect_dcm_assoc_item(tvb, pinfo, pctx_ptree, offset-4, "Transfer Syntax: ", DCM_ITEM_VALUE_TYPE_UID, &pctx_xfer_uid, &pctx_xfer_desc, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_pctx_xfer_syntax, ett_assoc_pctx_xfer); /* In a correct Association Response, only one Transfer syntax shall be present. Therefore, pctx_xfer_uid, pctx_xfer_desc are used for the accept scenario in the info mode */ if (!is_assoc_request && pctx_result == 0) { /* Association Response, Context Accepted */ dcm_set_syntax(pctx, pctx_xfer_uid, pctx_xfer_desc); } cnt_xfer += 1; offset += item_len; break; default: offset += item_len; break; } } if (is_assoc_request) { if (cnt_abbs<1) { expert_add_info(pinfo, pctx_pitem, &ei_dcm_no_abstract_syntax); return; } else if (cnt_abbs>1) { expert_add_info(pinfo, pctx_pitem, &ei_dcm_multiple_abstract_syntax); return; } if (cnt_xfer==0) { expert_add_info(pinfo, pctx_pitem, &ei_dcm_no_transfer_syntax); return; } if (pctx_abss_uid==NULL) { expert_add_info(pinfo, pctx_pitem, &ei_dcm_no_abstract_syntax_uid); return; } } else { if (cnt_xfer>1) { expert_add_info(pinfo, pctx_pitem, &ei_dcm_multiple_transfer_syntax); return; } } if (pctx->abss_uid==NULL) { /* Permanent copy information into structure */ pctx->abss_uid = wmem_strdup(wmem_file_scope(), pctx_abss_uid); pctx->abss_desc = wmem_strdup(wmem_file_scope(), pctx_abss_desc); } /* Copy to buffer first, because proto_item_append_text() crashed for an unknown reason using 'ID 0x%02x, %s, %s' and in my opinion correctly set parameters. */ if (is_assoc_request) { if (pctx_abss_desc == NULL) { buf_desc = pctx_abss_uid; } else { buf_desc = wmem_strdup_printf(pinfo->pool, "%s (%s)", pctx_abss_desc, pctx_abss_uid); } } else { if (pctx_result==0) { /* Accepted */ buf_desc = wmem_strdup_printf(pinfo->pool, "ID 0x%02x, %s, %s, %s", pctx_id, pctx_result_desc, dcm_uid_or_desc(pctx->xfer_uid, pctx->xfer_desc), dcm_uid_or_desc(pctx->abss_uid, pctx->abss_desc)); } else { /* Rejected */ buf_desc = wmem_strdup_printf(pinfo->pool, "ID 0x%02x, %s, %s", pctx_id, pctx_result_desc, dcm_uid_or_desc(pctx->abss_uid, pctx->abss_desc)); } } proto_item_append_text(pctx_pitem, "%s", buf_desc); } /* Decode the user info item in a Association Request or Response */ static void dissect_dcm_userinfo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset, uint32_t len, const char *pitem_prefix) { proto_item *userinfo_pitem = NULL; proto_tree *userinfo_ptree = NULL; /* Tree for presentation context details */ uint8_t item_type; uint16_t item_len; bool first_item=true; char *info_max_pdu=NULL; char *info_impl_uid=NULL; char *info_impl_version=NULL; const char *dummy=NULL; uint32_t endpos; endpos = offset + len; item_type = tvb_get_uint8(tvb, offset-4); item_len = tvb_get_ntohs(tvb, offset-2); userinfo_pitem = proto_tree_add_item(tree, hf_dcm_info, tvb, offset-4, item_len+4, ENC_NA); proto_item_set_text(userinfo_pitem, "%s", pitem_prefix); userinfo_ptree = proto_item_add_subtree(userinfo_pitem, ett_assoc_info); proto_tree_add_uint(userinfo_ptree, hf_dcm_assoc_item_type, tvb, offset-4, 1, item_type); /* The type is only one byte long */ proto_tree_add_uint(userinfo_ptree, hf_dcm_assoc_item_len, tvb, offset-2, 2, item_len); while (offset < endpos) { item_type = tvb_get_uint8(tvb, offset); item_len = tvb_get_ntohs(tvb, 2 + offset); offset += 4; switch (item_type) { case 0x51: /* Max length */ dissect_dcm_assoc_item(tvb, pinfo, userinfo_ptree, offset-4, "Max PDU Length: ", DCM_ITEM_VALUE_TYPE_UINT32, &info_max_pdu, &dummy, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_pdu_maxlen, ett_assoc_info_uid); if (!first_item) { proto_item_append_text(userinfo_pitem, ", "); } proto_item_append_text(userinfo_pitem, "Max PDU Length %s", info_max_pdu); first_item=false; offset += item_len; break; case 0x52: /* UID */ /* Parse Item. Works also in info mode where dcm_pctx_tree is NULL */ dissect_dcm_assoc_item(tvb, pinfo, userinfo_ptree, offset-4, "Implementation UID: ", DCM_ITEM_VALUE_TYPE_STRING, &info_impl_uid, &dummy, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_info_uid, ett_assoc_info_uid); if (!first_item) { proto_item_append_text(userinfo_pitem, ", "); } proto_item_append_text(userinfo_pitem, "Implementation UID %s", info_impl_uid); first_item=false; offset += item_len; break; case 0x55: /* version */ dissect_dcm_assoc_item(tvb, pinfo, userinfo_ptree, offset-4, "Implementation Version: ", DCM_ITEM_VALUE_TYPE_STRING, &info_impl_version, &dummy, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_info_version, ett_assoc_info_version); if (!first_item) { proto_item_append_text(userinfo_pitem, ", "); } proto_item_append_text(userinfo_pitem, "Version %s", info_impl_version); first_item=false; offset += item_len; break; case 0x53: /* async negotiation */ dissect_dcm_assoc_async_negotiation(tvb, userinfo_ptree, offset-4); offset += item_len; break; case 0x54: /* scp/scu role selection */ dissect_dcm_assoc_role_selection(tvb, pinfo, userinfo_ptree, offset-4); offset += item_len; break; case 0x56: /* extended negotiation */ dissect_dcm_assoc_sopclass_extneg(tvb, pinfo, userinfo_ptree, offset-4); offset += item_len; break; case 0x58: /* User Identify */ dissect_dcm_assoc_user_identify(tvb, pinfo, userinfo_ptree, offset-4); offset += item_len; break; default: dissect_dcm_assoc_unknown(tvb, userinfo_ptree, offset-4); offset += item_len; break; } } } /* Create a subtree for association requests or responses */ static uint32_t dissect_dcm_assoc_detail(tvbuff_t *tvb, packet_info *pinfo, proto_item *ti, dcm_state_assoc_t *assoc, uint32_t offset, uint32_t len) { proto_tree *assoc_tree = NULL; /* Tree for PDU details */ uint8_t item_type; uint16_t item_len; uint32_t endpos; char *item_value = NULL; const char *item_description = NULL; endpos = offset + len; assoc_tree = proto_item_add_subtree(ti, ett_assoc); while (offset < endpos) { item_type = tvb_get_uint8(tvb, offset); item_len = tvb_get_ntohs(tvb, 2 + offset); if (item_len == 0) { expert_add_info(pinfo, ti, &ei_dcm_assoc_item_len); return endpos; } offset += 4; switch (item_type) { case 0x10: /* Application context */ dissect_dcm_assoc_item(tvb, pinfo, assoc_tree, offset-4, "Application Context: ", DCM_ITEM_VALUE_TYPE_UID, &item_value, &item_description, &hf_dcm_assoc_item_type, &hf_dcm_assoc_item_len, &hf_dcm_actx, ett_assoc_actx); offset += item_len; break; case 0x20: /* Presentation context request */ dissect_dcm_pctx(tvb, pinfo, assoc_tree, assoc, offset, item_len, "Presentation Context: ", true); offset += item_len; break; case 0x21: /* Presentation context reply */ dissect_dcm_pctx(tvb, pinfo, assoc_tree, assoc, offset, item_len, "Presentation Context: ", false); offset += item_len; break; case 0x50: /* User Info */ dissect_dcm_userinfo(tvb, pinfo, assoc_tree, offset, item_len, "User Info: "); offset += item_len; break; default: offset += item_len; break; } } return offset; } static uint32_t dissect_dcm_pdv_header(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_assoc_t *assoc, uint32_t offset, dcm_state_pdv_t **pdv) { /* Dissect Context and Flags of a PDV and create new PDV structure */ proto_item *pdv_ctx_pitem = NULL; proto_item *pdv_flags_pitem = NULL; dcm_state_pctx_t *pctx = NULL; dcm_state_pdv_t *pdv_first_data = NULL; const char *desc_flag = NULL; /* Flag Description in tree */ char *desc_header = NULL; /* Used for PDV description */ uint8_t flags = 0, o_flags = 0; uint8_t pctx_id = 0; /* 1 Byte Context */ pctx_id = tvb_get_uint8(tvb, offset); pctx = dcm_state_pctx_get(assoc, pctx_id, false); if (pctx && pctx->xfer_uid) { proto_tree_add_uint_format(tree, hf_dcm_pdv_ctx, tvb, offset, 1, pctx_id, "Context: 0x%02x (%s, %s)", pctx_id, dcm_uid_or_desc(pctx->xfer_uid, pctx->xfer_desc), dcm_uid_or_desc(pctx->abss_uid, pctx->abss_desc)); } else { pdv_ctx_pitem=proto_tree_add_uint_format(tree, hf_dcm_pdv_ctx, tvb, offset, 1, pctx_id, "Context: 0x%02x not found. A-ASSOCIATE request not found in capture.", pctx_id); expert_add_info(pinfo, pdv_ctx_pitem, &ei_dcm_pdv_ctx); if (pctx == NULL) { /* only create presentation context, if it does not yet exist */ /* Create fake PCTX and guess Syntax ILE, ELE, EBE */ pctx = dcm_state_pctx_new(assoc, pctx_id); /* To be done: Guess Syntax */ pctx->syntax = DCM_UNK; } } offset +=1; /* Create PDV structure: Since we can have multiple PDV per packet (offset) and multiple merged packets per PDV (the tvb raw_offset) we need both values to uniquely identify a PDV */ *pdv = dcm_state_pdv_get(pctx, pinfo->num, tvb_raw_offset(tvb)+offset, true); if (*pdv == NULL) { return 0; /* Failed to allocate memory */ } /* 1 Byte Flag */ /* PS3.8 E.2 Bits 2 through 7 are always set to 0 by the sender and never checked by the receiver. */ o_flags = tvb_get_uint8(tvb, offset); flags = 0x3 & o_flags; (*pdv)->pctx_id = pctx_id; switch (flags) { case 0: /* 00 */ if (0 != (0xfc & o_flags)) desc_flag = "Data, More Fragments (Warning: Invalid)"; else desc_flag = "Data, More Fragments"; (*pdv)->is_flagvalid = true; (*pdv)->is_command = false; (*pdv)->is_last_fragment = false; (*pdv)->syntax = pctx->syntax; /* Inherit syntax for data PDVs*/ break; case 2: /* 10 */ if (0 != (0xfc & o_flags)) desc_flag = "Data, Last Fragment (Warning: Invalid)"; else desc_flag = "Data, Last Fragment"; (*pdv)->is_flagvalid = true; (*pdv)->is_command = false; (*pdv)->is_last_fragment = true; (*pdv)->syntax = pctx->syntax; /* Inherit syntax for data PDVs*/ break; case 1: /* 01 */ if (0 != (0xfc & o_flags)) desc_flag = "Command, More Fragments (Warning: Invalid)"; else desc_flag = "Command, More Fragments"; desc_header = wmem_strdup(wmem_file_scope(), "Command"); /* Will be overwritten with real command tag */ (*pdv)->is_flagvalid = true; (*pdv)->is_command = true; (*pdv)->is_last_fragment = false; (*pdv)->syntax = DCM_ILE; /* Command tags are always little endian*/ break; case 3: /* 11 */ if (0 != (0xfc & o_flags)) desc_flag = "Command, Last Fragment (Warning: Invalid)"; else desc_flag = "Command, Last Fragment"; desc_header = wmem_strdup(wmem_file_scope(), "Command"); (*pdv)->is_flagvalid = true; (*pdv)->is_command = true; (*pdv)->is_last_fragment = true; (*pdv)->syntax = DCM_ILE; /* Command tags are always little endian*/ break; default: desc_flag = "Invalid Flags"; desc_header = wmem_strdup(wmem_file_scope(), desc_flag); (*pdv)->is_flagvalid = false; (*pdv)->is_command = false; (*pdv)->is_last_fragment = false; (*pdv)->syntax = DCM_UNK; } if (!PINFO_FD_VISITED(pinfo)) { (*pdv)->reassembly_id = pctx->reassembly_count; if ((*pdv)->is_last_fragment) { pctx->reassembly_count++; } } if (flags == 0 || flags == 2) { /* Data PDV */ pdv_first_data = dcm_state_pdv_get_obj_start(*pdv); if (pdv_first_data->prev && pdv_first_data->prev->is_command) { /* Every Data PDV sequence should be preceded by a Command PDV, so we should always hit this for a correct capture */ if (pctx->abss_desc && g_str_has_suffix(pctx->abss_desc, "Storage")) { /* Should be done far more intelligent, e.g. does not catch the (Retired) ones */ if (flags == 0) { desc_header = wmem_strdup_printf(wmem_file_scope(), "%s Fragment", pctx->abss_desc); } else { desc_header = wmem_strdup(wmem_file_scope(), pctx->abss_desc); } (*pdv)->is_storage = true; } else { /* Use previous command and append DATA*/ desc_header = wmem_strdup_printf(wmem_file_scope(), "%s-DATA", pdv_first_data->prev->desc); } } else { desc_header = wmem_strdup(wmem_file_scope(), "DATA"); } } (*pdv)->desc = desc_header; pdv_flags_pitem = proto_tree_add_uint_format(tree, hf_dcm_pdv_flags, tvb, offset, 1, flags, "Flags: 0x%02x (%s)", o_flags, desc_flag); if (o_flags>3) { expert_add_info(pinfo, pdv_flags_pitem, &ei_dcm_pdv_flags); } offset +=1; return offset; } /* Based on the value representation, decode the value of one tag. Support VM>1 for most types, but not all. Returns new offset */ static uint32_t dissect_dcm_tag_value(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_pdv_t *pdv, uint32_t offset, uint16_t grp, uint16_t elm, uint32_t vl, uint32_t vl_max, const char* vr, char **tag_value) { proto_item *pitem = NULL; unsigned encoding = (pdv->syntax == DCM_EBE) ? ENC_BIG_ENDIAN : ENC_LITTLE_ENDIAN; /* Make sure we have all the bytes of the item; this should throw and exception if vl_max is so large that it causes the offset to overflow. */ tvb_ensure_bytes_exist(tvb, offset, vl_max); /* --------------------------------------------------------------------------- Potentially long types. Obey vl_max --------------------------------------------------------------------------- */ if ((strncmp(vr, "AE", 2) == 0) || (strncmp(vr, "AS", 2) == 0) || (strncmp(vr, "CS", 2) == 0) || (strncmp(vr, "DA", 2) == 0) || (strncmp(vr, "DS", 2) == 0) || (strncmp(vr, "DT", 2) == 0) || (strncmp(vr, "IS", 2) == 0) || (strncmp(vr, "LO", 2) == 0) || (strncmp(vr, "LT", 2) == 0) || (strncmp(vr, "PN", 2) == 0) || (strncmp(vr, "SH", 2) == 0) || (strncmp(vr, "ST", 2) == 0) || (strncmp(vr, "TM", 2) == 0) || (strncmp(vr, "UI", 2) == 0) || (strncmp(vr, "UT", 2) == 0) ) { /* 15 ways to represent a string. For LT, ST, UT the DICOM standard does not allow multi-value For the others, VM is built into 'automatically, because it uses '\' as separator */ char *vals; dcm_uid_t const *uid = NULL; uint8_t val8; val8 = tvb_get_uint8(tvb, offset + vl_max - 1); if (val8 == 0x00) { /* Last byte of string is 0x00, i.e. padded */ vals = tvb_format_text(pinfo->pool, tvb, offset, vl_max - 1); } else { vals = tvb_format_text(pinfo->pool, tvb, offset, vl_max); } if (grp == 0x0000 && elm == 0x0902) { /* The error comment */ pdv->comment = g_strstrip(wmem_strdup(wmem_file_scope(), vals)); } if ((strncmp(vr, "UI", 2) == 0)) { /* This is a UID. Attempt a lookup. Will only return something for classes of course */ uid = (dcm_uid_t const *)wmem_map_lookup(dcm_uid_table, (void *) vals); if (uid) { *tag_value = wmem_strdup_printf(pinfo->pool, "%s (%s)", vals, uid->name); } else { *tag_value = vals; } } else { if (strlen(vals) > 50) { *tag_value = wmem_strdup_printf(pinfo->pool, "%s%s", ws_utf8_truncate(vals, 50), UTF8_HORIZONTAL_ELLIPSIS); } else { *tag_value = vals; } } proto_tree_add_string(tree, hf_dcm_tag_value_str, tvb, offset, vl_max, *tag_value); } else if ((strncmp(vr, "OB", 2) == 0) || (strncmp(vr, "OW", 2) == 0) || (strncmp(vr, "OF", 2) == 0) || (strncmp(vr, "OD", 2) == 0)) { /* Array of Bytes, Words, Float, or Doubles. Don't perform any decoding. VM=1. Multiple arrays are not possible */ proto_tree_add_bytes_format_value(tree, hf_dcm_tag_value_byte, tvb, offset, vl_max, NULL, "%s", "(binary)"); *tag_value = wmem_strdup(pinfo->pool, "(binary)"); } else if (strncmp(vr, "UN", 2) == 0) { /* Usually the case for private tags in implicit syntax, since tag was not found and VR not specified. Not been able to create UN yet. No need to support VM > 1. */ uint8_t val8; char *vals; uint32_t i; /* String detector, i.e. check if we only have alpha-numeric character */ bool is_string = true; bool is_padded = false; for (i = 0; i < vl_max ; i++) { val8 = tvb_get_uint8(tvb, offset + i); if ((val8 == 0x09) || (val8 == 0x0A) || (val8 == 0x0D)) { /* TAB, LF, CR */ } else if ((val8 >= 0x20) && (val8 <= 0x7E)) { /* No extended ASCII, 0-9, A-Z, a-z */ } else if ((i == vl_max -1) && (val8 == 0x00)) { /* Last Byte can be null*/ is_padded = true; } else { /* Here's the code */ is_string = false; } } if (is_string) { vals = tvb_format_text(pinfo->pool, tvb, offset, (is_padded ? vl_max - 1 : vl_max)); proto_tree_add_string(tree, hf_dcm_tag_value_str, tvb, offset, vl_max, vals); *tag_value = vals; } else { proto_tree_add_bytes_format_value(tree, hf_dcm_tag_value_byte, tvb, offset, vl_max, NULL, "%s", "(binary)"); *tag_value = wmem_strdup(pinfo->pool, "(binary)"); } } /* --------------------------------------------------------------------------- Smaller types. vl/vl_max are not used. Fixed item length from 2 to 8 bytes --------------------------------------------------------------------------- */ else if (strncmp(vr, "AT", 2) == 0) { /* Attribute Tag e.g. (0022,8866). 2*2 Bytes, Can have VM > 1 */ uint16_t at_grp; uint16_t at_elm; char *at_value = ""; /* In on capture the reported length for this tag was 2 bytes. And since vl_max is unsigned long, -3 caused it to be 2^32-1 So make it at least one loop so set it to at least 4. */ uint32_t vm_item_len = 4; uint32_t vm_item_count = dcm_vm_item_count(vl_max, vm_item_len); uint32_t i = 0; while (i < vm_item_count) { at_grp = tvb_get_uint16(tvb, offset+ i*vm_item_len, encoding); at_elm = tvb_get_uint16(tvb, offset+ i*vm_item_len+2, encoding); proto_tree_add_uint_format_value(tree, hf_dcm_tag_value_32u, tvb, offset + i*vm_item_len, vm_item_len, (at_grp << 16) | at_elm, "%04x,%04x", at_grp, at_elm); at_value = wmem_strdup_printf(pinfo->pool,"%s(%04x,%04x)", at_value, at_grp, at_elm); i++; } *tag_value = at_value; } else if (strncmp(vr, "FL", 2) == 0) { /* Single Float. Can be VM > 1, but not yet supported */ float valf = tvb_get_ieee_float(tvb, offset, encoding); proto_tree_add_bytes_format_value(tree, hf_dcm_tag_value_byte, tvb, offset, 4, NULL, "%f", valf); *tag_value = wmem_strdup_printf(pinfo->pool, "%f", valf); } else if (strncmp(vr, "FD", 2) == 0) { /* Double Float. Can be VM > 1, but not yet supported */ double vald = tvb_get_ieee_double(tvb, offset, encoding); proto_tree_add_bytes_format_value(tree, hf_dcm_tag_value_byte, tvb, offset, 8, NULL, "%f", vald); *tag_value = wmem_strdup_printf(pinfo->pool, "%f", vald); } else if (strncmp(vr, "SL", 2) == 0) { /* Signed Long. Can be VM > 1, but not yet supported */ int32_t val32; proto_tree_add_item_ret_int(tree, hf_dcm_tag_value_32s, tvb, offset, 4, encoding, &val32); *tag_value = wmem_strdup_printf(pinfo->pool, "%d", val32); } else if (strncmp(vr, "SS", 2) == 0) { /* Signed Short. Can be VM > 1, but not yet supported */ int32_t val32; proto_tree_add_item_ret_int(tree, hf_dcm_tag_value_16s, tvb, offset, 2, encoding, &val32); *tag_value = wmem_strdup_printf(pinfo->pool, "%d", val32); } else if (strncmp(vr, "UL", 2) == 0) { /* Unsigned Long. Can be VM > 1, but not yet supported */ uint32_t val32; proto_tree_add_item_ret_uint(tree, hf_dcm_tag_value_32u, tvb, offset, 4, encoding, &val32); *tag_value = wmem_strdup_printf(pinfo->pool, "%u", val32); } else if (strncmp(vr, "US", 2) == 0) { /* Unsigned Short. Can be VM > 1, but not yet supported */ const char *status_message = NULL; uint16_t val16 = tvb_get_uint16(tvb, offset, encoding); if (grp == 0x0000 && elm == 0x0100) { /* This is a command */ pdv->command = wmem_strdup(wmem_file_scope(), val_to_str_const(val16, dcm_cmd_vals, " ")); *tag_value = pdv->command; } else if (grp == 0x0000 && elm == 0x0900) { /* This is a status message. If value is not 0x0000, add an expert info */ status_message = dcm_rsp2str(val16); *tag_value = wmem_strdup_printf(pinfo->pool, "%s (0x%02x)", status_message, val16); if ((val16 & 0xFF00) == 0xFF00) { /* C-FIND also has a 0xFF01 as a valid response */ pdv->is_pending = true; } else if (val16 != 0x0000) { /* Neither success nor pending */ pdv->is_warning = true; } pdv->status = wmem_strdup(wmem_file_scope(), status_message); } else { *tag_value = wmem_strdup_printf(pinfo->pool, "%u", val16); } if (grp == 0x0000) { if (elm == 0x0110) { /* (0000,0110) Message ID */ pdv->message_id = val16; } else if (elm == 0x0120) { /* (0000,0120) Message ID Being Responded To */ pdv->message_id_resp = val16; } else if (elm == 0x1020) { /* (0000,1020) Number of Remaining Sub-operations */ pdv->no_remaining = val16; } else if (elm == 0x1021) { /* (0000,1021) Number of Completed Sub-operations */ pdv->no_completed = val16; } else if (elm == 0x1022) { /* (0000,1022) Number of Failed Sub-operations */ pdv->no_failed = val16; } else if (elm == 0x1023) { /* (0000,1023) Number of Warning Sub-operations */ pdv->no_warning = val16; } } pitem = proto_tree_add_uint_format_value(tree, hf_dcm_tag_value_16u, tvb, offset, 2, val16, "%s", *tag_value); if (pdv->is_warning && status_message) { expert_add_info(pinfo, pitem, &ei_dcm_status_msg); } } /* Invalid VR, can only occur with Explicit syntax */ else { proto_tree_add_bytes_format_value(tree, hf_dcm_tag_value_byte, tvb, offset, vl_max, NULL, "%s", (vl > vl_max ? "" : "(unknown VR)")); *tag_value = wmem_strdup(pinfo->pool, "(unknown VR)"); } offset += vl_max; return offset; } /* Return true, if the required size does not fit at position 'offset'. */ static bool dcm_tag_is_open(dcm_state_pdv_t *pdv, uint32_t startpos, uint32_t offset, uint32_t endpos, uint32_t size_required) { if (offset + size_required > endpos) { pdv->open_tag.is_header_fragmented = true; pdv->open_tag.len_decoded = endpos - startpos; return true; } else { return false; } } static dcm_tag_t const * dcm_tag_lookup(uint16_t grp, uint16_t elm) { static dcm_tag_t const *tag_def = NULL; static dcm_tag_t const tag_unknown = { 0x00000000, "(unknown)", "UN", "1", 0, 0}; static dcm_tag_t const tag_private = { 0x00000000, "Private Tag", "UN", "1", 0, 0 }; static dcm_tag_t const tag_private_grp_len = { 0x00000000, "Private Tag Group Length", "UL", "1", 0, 0 }; static dcm_tag_t const tag_grp_length = { 0x00000000, "Group Length", "UL", "1", 0, 0 }; /* Try a direct hit first before doing a masked search */ tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | elm)); if (tag_def == NULL) { /* No match found */ if ((grp & 0x0001) && (elm == 0x0000)) { tag_def = &tag_private_grp_len; } else if (grp & 0x0001) { tag_def = &tag_private; } else if (elm == 0x0000) { tag_def = &tag_grp_length; } /* There are a few tags that require a mask to be found */ else if (((grp & 0xFF00) == 0x5000) || ((grp & 0xFF00) == 0x6000) || ((grp & 0xFF00) == 0x7F00)) { /* Do a special for groups 0x50xx, 0x60xx and 0x7Fxx */ tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER((((uint32_t)grp & 0xFF00) << 16) | elm)); } else if ((grp == 0x0020) && ((elm & 0xFF00) == 0x3100)) { tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | (elm & 0xFF00))); } else if ((grp == 0x0028) && ((elm & 0xFF00) == 0x0400)) { /* This map was done to 0x041x */ tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | (elm & 0xFF0F) | 0x0010)); } else if ((grp == 0x0028) && ((elm & 0xFF00) == 0x0800)) { tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | (elm & 0xFF0F))); } else if (grp == 0x1000) { tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | (elm & 0x000F))); } else if (grp == 0x1010) { tag_def = (dcm_tag_t const *)wmem_map_lookup(dcm_tag_table, GUINT_TO_POINTER(((uint32_t)grp << 16) | (elm & 0x0000))); } if (tag_def == NULL) { /* Still no match found */ tag_def = &tag_unknown; } } return tag_def; } static char* dcm_tag_summary(packet_info *pinfo, uint16_t grp, uint16_t elm, uint32_t vl, const char *tag_desc, const char *vr, bool is_retired, bool is_implicit) { char *desc_mod; char *tag_vl; char *tag_sum; if (is_retired) { desc_mod = wmem_strdup_printf(pinfo->pool, "(Retired) %-35.35s", tag_desc); } else { desc_mod = wmem_strdup_printf(pinfo->pool, "%-45.45s", tag_desc); } if (vl == 0xFFFFFFFF) { tag_vl = wmem_strdup_printf(pinfo->pool, "%10.10s", ""); } else { tag_vl = wmem_strdup_printf(pinfo->pool, "%10u", vl); /* Show as dec */ } if (is_implicit) tag_sum = wmem_strdup_printf(pinfo->pool, "(%04x,%04x) %s %s", grp, elm, tag_vl, desc_mod); else tag_sum = wmem_strdup_printf(pinfo->pool, "(%04x,%04x) %s %s [%s]", grp, elm, tag_vl, desc_mod, vr); return tag_sum; } /* Decode one tag. If it is a sequence or item start create a subtree. Returns new offset. http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html */ static uint32_t // NOLINTNEXTLINE(misc-no-recursion) dissect_dcm_tag(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_pdv_t *pdv, uint32_t offset, uint32_t endpos, bool is_first_tag, const char **tag_description, bool *end_of_seq_or_item) { proto_tree *tag_ptree = NULL; /* Tree for decoded tag details */ proto_tree *seq_ptree = NULL; /* Possible subtree for sequences and items */ proto_item *tag_pitem = NULL; dcm_tag_t const *tag_def = NULL; int ett; const char *vr = NULL; char *tag_value = ""; /* Tag Value converted to a string */ char *tag_summary; uint32_t vl = 0; uint16_t vl_1 = 0; uint16_t vl_2 = 0; uint32_t offset_tag = 0; /* Remember offsets for tree, since the tree */ uint32_t offset_vr = 0; /* header is created pretty late */ uint32_t offset_vl = 0; uint32_t vl_max = 0; /* Max Value Length to Parse */ uint16_t grp = 0; uint16_t elm = 0; uint32_t len_decoded_remaing = 0; /* Decode the syntax a little more */ uint32_t encoding = (pdv->syntax == DCM_EBE) ? ENC_BIG_ENDIAN : ENC_LITTLE_ENDIAN; bool is_implicit = (pdv->syntax == DCM_ILE); bool is_vl_long = false; /* True for 4 Bytes length fields */ bool is_sequence = false; /* True for Sequence Tags */ bool is_item = false; /* True for Sequence Item Tags */ *tag_description = NULL; /* Reset description. It's wmem packet scope memory, so not really bad*/ offset_tag = offset; if (pdv->prev && is_first_tag) { len_decoded_remaing = pdv->prev->open_tag.len_decoded; } /* Since we may have a fragmented header, check for every attribute, whether we have already decoded left-overs from the previous PDV. Since we have implicit & explicit syntax, copying the open tag to a buffer without decoding, would have caused tvb_get_xxtohs() implementations on the copy. An alternative approach would have been to resemble the PDVs first. The attempts to reassemble without named sources (to be implemented) were very sensitive to missing packets. In such a case, no packet of a PDV chain was decoded, not even the start. So for the time being, use this rather cumbersome approach. For every two bytes (PDV length are always a factor of 2) check whether we have enough data in the buffer and store the value accordingly. In the next frame check, whether we have decoded this yet. */ /* Group */ if (len_decoded_remaing >= 2) { grp = pdv->prev->open_tag.grp; len_decoded_remaing -= 2; } else { if (dcm_tag_is_open(pdv, offset_tag, offset, endpos, 2)) return endpos; /* Exit if needed */ grp = tvb_get_uint16(tvb, offset, encoding); offset += 2; pdv->open_tag.grp = grp; } /* Element */ if (len_decoded_remaing >= 2) { elm = pdv->prev->open_tag.elm; len_decoded_remaing -= 2; } else { if (dcm_tag_is_open(pdv, offset_tag, offset, endpos, 2)) return endpos; /* Exit if needed */ elm = tvb_get_uint16(tvb, offset, encoding); offset += 2; pdv->open_tag.elm = elm; } /* Find the best matching tag */ tag_def = dcm_tag_lookup(grp, elm); /* Value Representation */ offset_vr = offset; if ((grp == 0xFFFE) && (elm == 0xE000 || elm == 0xE00D || elm == 0xE0DD)) { /* Item start, Item Delimitation or Sequence Delimitation */ vr = "UL"; is_vl_long = true; /* These tags always have a 4 byte length field */ } else if (is_implicit) { /* Get VR from tag definition */ vr = wmem_strdup(pinfo->pool, tag_def->vr); is_vl_long = true; /* Implicit always has 4 byte length field */ } else { if (len_decoded_remaing >= 2) { vr = wmem_strdup(pinfo->pool, pdv->prev->open_tag.vr); len_decoded_remaing -= 2; } else { /* Controlled exit, if VR does not fit. */ if (dcm_tag_is_open(pdv, offset_tag, offset_vr, endpos, 2)) return endpos; vr = (char *)tvb_get_string_enc(pinfo->pool, tvb, offset, 2, ENC_ASCII); offset += 2; wmem_free(wmem_file_scope(), pdv->open_tag.vr); pdv->open_tag.vr = wmem_strdup(wmem_file_scope(), vr); /* needs to survive within a session */ } if ((strcmp(vr, "OB") == 0) || (strcmp(vr, "OW") == 0) || (strcmp(vr, "OF") == 0) || (strcmp(vr, "OD") == 0) || (strcmp(vr, "OL") == 0) || (strcmp(vr, "SQ") == 0) || (strcmp(vr, "UC") == 0) || (strcmp(vr, "UR") == 0) || (strcmp(vr, "UT") == 0) || (strcmp(vr, "UN") == 0)) { /* Part 5, Table 7.1-1 in the standard */ /* Length is always 4 bytes: OB, OD, OF, OL, OW, SQ, UC, UR, UT or UN */ is_vl_long = true; /* Skip 2 Bytes */ if (len_decoded_remaing >= 2) { len_decoded_remaing -= 2; } else { if (dcm_tag_is_open(pdv, offset_tag, offset_vr, endpos, 2)) return endpos; offset += 2; } } else { is_vl_long = false; } } /* Value Length. This is rather cumbersome code to get a 4 byte length, but in the fragmented case, we have 2*2 bytes. So always use that pattern */ offset_vl = offset; if (len_decoded_remaing >= 2) { vl_1 = pdv->prev->open_tag.vl_1; len_decoded_remaing -= 2; } else { if (dcm_tag_is_open(pdv, offset_tag, offset_vl, endpos, 2)) return endpos; vl_1 = tvb_get_uint16(tvb, offset, encoding); offset += 2; pdv->open_tag.vl_1 = vl_1; } if (is_vl_long) { if (len_decoded_remaing >= 2) { vl_2 = pdv->prev->open_tag.vl_2; } else { if (dcm_tag_is_open(pdv, offset_tag, offset_vl+2, endpos, 2)) return endpos; vl_2 = tvb_get_uint16(tvb, offset, encoding); offset += 2; pdv->open_tag.vl_2 = vl_2; } if (encoding == ENC_LITTLE_ENDIAN) vl = (vl_2 << 16) + vl_1; else vl = (vl_1 << 16) + vl_2; } else { vl = vl_1; } /* Now we have most of the information, except for sequences and items with undefined length :-/. But, whether we know the length or not, we now need to create the tree item and subtree, before we can loop into sequences and items Display the information we collected so far. Don't wait until the value is parsed, because that parsing might cause an exception. If that happens within a sequence, the sequence tag would not show up with the value Use different ett_ for Sequences & Items, so that fold/unfold state makes sense */ tag_summary = dcm_tag_summary(pinfo, grp, elm, vl, tag_def->description, vr, tag_def->is_retired, is_implicit); is_sequence = (strcmp(vr, "SQ") == 0) || (vl == 0xFFFFFFFF); is_item = ((grp == 0xFFFE) && (elm == 0xE000)); if ((is_sequence | is_item) && global_dcm_seq_subtree) { ett = is_sequence ? ett_dcm_data_seq : ett_dcm_data_item; } else { ett = ett_dcm_data_tag; } if (vl == 0xFFFFFFFF) { /* 'Just' mark header as the length of the item */ tag_ptree = proto_tree_add_subtree(tree, tvb, offset_tag, offset - offset_tag, ett, &tag_pitem, tag_summary); vl_max = 0; /* We don't know who long this sequence/item is */ } else if (offset + vl <= endpos) { /* Show real length of item */ tag_ptree = proto_tree_add_subtree(tree, tvb, offset_tag, offset + vl - offset_tag, ett, &tag_pitem, tag_summary); vl_max = vl; } else { /* Value is longer than what we have in the PDV, -> we do have a OPEN tag */ tag_ptree = proto_tree_add_subtree(tree, tvb, offset_tag, endpos - offset_tag, ett, &tag_pitem, tag_summary); vl_max = endpos - offset; } /* If you are going to touch the following 25 lines, make sure you reserve a few hours to go through both display options and check for proper tree display :-) */ if (is_sequence | is_item) { if (global_dcm_seq_subtree) { /* Use different ett_ for Sequences & Items, so that fold/unfold state makes sense */ seq_ptree = tag_ptree; if (!global_dcm_tag_subtree) { tag_ptree = NULL; } } else { seq_ptree = tree; if (!global_dcm_tag_subtree) { tag_ptree = NULL; } } } else { /* For tags */ if (!global_dcm_tag_subtree) { tag_ptree = NULL; } } /* --------------------------------------------------------------- Tag details as separate items --------------------------------------------------------------- */ proto_tree_add_uint_format_value(tag_ptree, hf_dcm_tag, tvb, offset_tag, 4, (grp << 16) | elm, "%04x,%04x (%s)", grp, elm, tag_def->description); /* Add VR to tag detail, except for sequence items */ if (!is_item) { if (is_implicit) { /* Select header, since no VR is present in implicit syntax */ proto_tree_add_string(tag_ptree, hf_dcm_tag_vr, tvb, offset_tag, 4, vr); } else { proto_tree_add_string(tag_ptree, hf_dcm_tag_vr, tvb, offset_vr, 2, vr); } } /* Add length to tag detail */ proto_tree_add_uint(tag_ptree, hf_dcm_tag_vl, tvb, offset_vl, (is_vl_long ? 4 : 2), vl); /* --------------------------------------------------------------- Finally the Tag Value --------------------------------------------------------------- */ if ((is_sequence || is_item) && (vl > 0)) { /* Sequence or Item Start */ uint32_t endpos_item = 0; bool local_end_of_seq_or_item = false; bool is_first_desc = true; const char *item_description = NULL; /* Will be allocated as wmem packet scope memory in dissect_dcm_tag() */ if (vl == 0xFFFFFFFF) { /* Undefined length */ increment_dissection_depth(pinfo); while ((!local_end_of_seq_or_item) && (!pdv->open_tag.is_header_fragmented) && (offset < endpos)) { offset = dissect_dcm_tag(tvb, pinfo, seq_ptree, pdv, offset, endpos, false, &item_description, &local_end_of_seq_or_item); if (item_description && global_dcm_seq_subtree) { proto_item_append_text(tag_pitem, (is_first_desc ? " %s" : ", %s"), item_description); is_first_desc = false; } } decrement_dissection_depth(pinfo); } else { /* Defined length */ endpos_item = offset + vl_max; increment_dissection_depth(pinfo); while (offset < endpos_item) { offset = dissect_dcm_tag(tvb, pinfo, seq_ptree, pdv, offset, endpos_item, false, &item_description, &local_end_of_seq_or_item); if (item_description && global_dcm_seq_subtree) { proto_item_append_text(tag_pitem, (is_first_desc ? " %s" : ", %s"), item_description); is_first_desc = false; } } decrement_dissection_depth(pinfo); } } /* if ((is_sequence || is_item) && (vl > 0)) */ else if ((grp == 0xFFFE) && (elm == 0xE00D)) { /* Item delimitation for items with undefined length */ *end_of_seq_or_item = true; } else if ((grp == 0xFFFE) && (elm == 0xE0DD)) { /* Sequence delimitation for sequences with undefined length */ *end_of_seq_or_item = true; } else if (vl == 0) { /* No value for this tag */ /* The following copy is needed. tag_value is post processed with g_strstrip() and that one will crash the whole application, when a constant is used. */ tag_value = wmem_strdup(pinfo->pool, ""); } else if (vl > vl_max) { /* Tag is longer than the PDV/PDU. Don't perform any decoding */ char *tag_desc; proto_tree_add_bytes_format(tag_ptree, hf_dcm_tag_value_byte, tvb, offset, vl_max, NULL, "%-8.8sBytes %d - %d [start]", "Value:", 1, vl_max); tag_value = wmem_strdup_printf(pinfo->pool, "", 1, vl_max); offset += vl_max; /* Save the needed data for reuse, and subsequent packets This will leak a little within the session. But since we may have tags being closed and reopen in the same PDV we will always need to store this */ tag_desc = dcm_tag_summary(pinfo, grp, elm, vl, tag_def->description, vr, tag_def->is_retired, is_implicit); if (pdv->open_tag.desc == NULL) { pdv->open_tag.is_value_fragmented = true; pdv->open_tag.desc = wmem_strdup(wmem_file_scope(), tag_desc); pdv->open_tag.len_total = vl; pdv->open_tag.len_remaining = vl - vl_max; } } else { /* Regular value. Identify the type, decode and display */ increment_dissection_depth(pinfo); offset = dissect_dcm_tag_value(tvb, pinfo, tag_ptree, pdv, offset, grp, elm, vl, vl_max, vr, &tag_value); decrement_dissection_depth(pinfo); /* ------------------------------------------------------------- We have decoded the value. Now store those tags of interest ------------------------------------------------------------- */ /* Store SOP Class and Instance UID in first PDV of this object */ if (grp == 0x0008 && elm == 0x0016) { dcm_state_pdv_get_obj_start(pdv)->sop_class_uid = wmem_strdup(wmem_file_scope(), tag_value); } else if (grp == 0x0008 && elm == 0x0018) { dcm_state_pdv_get_obj_start(pdv)->sop_instance_uid = wmem_strdup(wmem_file_scope(), tag_value); } else if (grp == 0x0000 && elm == 0x0100) { /* This is the command tag -> overwrite existing PDV description */ pdv->desc = wmem_strdup(wmem_file_scope(), tag_value); } } /* ------------------------------------------------------------------- Add the value to the already constructed item ------------------------------------------------------------------- */ proto_item_append_text(tag_pitem, " %s", tag_value); if (tag_def->add_to_summary) { *tag_description = wmem_strdup(pinfo->pool, g_strstrip(tag_value)); } return offset; } /* 'Decode' open tags from previous PDV. It mostly ends in 'continuation' or 'end' in the description. */ static uint32_t dissect_dcm_tag_open(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_pdv_t *pdv, uint32_t offset, uint32_t endpos, bool *is_first_tag) { proto_item *pitem = NULL; uint32_t tag_value_fragment_len = 0; if ((pdv->prev) && (pdv->prev->open_tag.len_remaining > 0)) { /* Not first PDV in the given presentation context (Those don't have remaining data to parse :-) */ /* And previous PDV has left overs, i.e. this is a continuation PDV */ if (endpos - offset >= pdv->prev->open_tag.len_remaining) { /* Remaining bytes are equal or more than we expect for the open tag Finally reach the end of this tag. Don't touch the open_tag structure of this PDV, as we may see a new open tag at the end */ tag_value_fragment_len = pdv->prev->open_tag.len_remaining; pdv->is_corrupt = false; } else if (pdv->is_flagvalid && pdv->is_last_fragment) { /* The tag is not yet complete, however, the flag indicates that it should be Therefore end this tag and issue an expert_add_info. Don't touch the open_tag structure of this PDV, as we may see a new open tag at the end */ tag_value_fragment_len = endpos - offset; pdv->is_corrupt = true; } else { /* * More to do for this tag */ tag_value_fragment_len = endpos - offset; /* Set data in current PDV structure */ if (!pdv->open_tag.is_value_fragmented) { /* No need to do it twice or more */ pdv->open_tag.is_value_fragmented = true; pdv->open_tag.len_total = pdv->prev->open_tag.len_total; pdv->open_tag.len_remaining = pdv->prev->open_tag.len_remaining - tag_value_fragment_len; pdv->open_tag.desc = wmem_strdup(wmem_file_scope(), pdv->prev->open_tag.desc); } pdv->is_corrupt = false; } if (pdv->is_corrupt) { pitem = proto_tree_add_bytes_format(tree, hf_dcm_data_tag, tvb, offset, tag_value_fragment_len, NULL, "%s ", pdv->prev->open_tag.desc); expert_add_info(pinfo, pitem, &ei_dcm_data_tag); } else { proto_tree_add_bytes_format(tree, hf_dcm_data_tag, tvb, offset, tag_value_fragment_len, NULL, "%s ", pdv->prev->open_tag.desc, pdv->prev->open_tag.len_total - pdv->prev->open_tag.len_remaining + 1, pdv->prev->open_tag.len_total - pdv->prev->open_tag.len_remaining + tag_value_fragment_len, (pdv->prev->open_tag.len_remaining > tag_value_fragment_len ? "continuation" : "end") ); } offset += tag_value_fragment_len; *is_first_tag = false; } return offset; } /* Decode the tag section inside a PDV. This can be a single combined dataset or DICOM natively split PDVs. Therefore it needs to resume previously opened tags. For data PDVs, only process tags when tree is set or listening to export objects tap. For command PDVs, process all tags. On export copy the content to the export buffer. */ static uint32_t dissect_dcm_pdv_body( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_assoc_t *assoc, dcm_state_pdv_t *pdv, uint32_t offset, uint32_t pdv_body_len, char **pdv_description) { const char *tag_value = NULL; bool dummy = false; uint32_t startpos = offset; uint32_t endpos = 0; endpos = offset + pdv_body_len; if (pdv->is_command || tree || have_tap_listener(dicom_eo_tap)) { /* Performance optimization starts here. Don't put any COL_INFO related stuff in here */ if (pdv->syntax == DCM_UNK) { /* Eventually, we will have a syntax detector. Until then, don't decode */ proto_tree_add_bytes_format(tree, hf_dcm_data_tag, tvb, offset, pdv_body_len, NULL, "(%04x,%04x) %-8x Unparsed data", 0, 0, pdv_body_len); } else { bool is_first_tag = true; /* Treat the left overs */ offset = dissect_dcm_tag_open(tvb, pinfo, tree, pdv, offset, endpos, &is_first_tag); /* Decode all tags, sequences and items in this PDV recursively */ while (offset < endpos) { offset = dissect_dcm_tag(tvb, pinfo, tree, pdv, offset, endpos, is_first_tag, &tag_value, &dummy); is_first_tag = false; } } } *pdv_description = pdv->desc; if (pdv->is_command) { if (pdv->is_warning) { if (pdv->comment) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s (%s, %s)", pdv->desc, pdv->status, pdv->comment); } else { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s (%s)", pdv->desc, pdv->status); } } else if (global_dcm_cmd_details) { /* Show command details in header */ if (pdv->message_id > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s ID=%d", pdv->desc, pdv->message_id); } else if (pdv->message_id_resp > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s ID=%d", pdv->desc, pdv->message_id_resp); if (pdv->no_completed > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s C=%d", *pdv_description, pdv->no_completed); } if (pdv->no_remaining > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s R=%d", *pdv_description, pdv->no_remaining); } if (pdv->no_warning > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s W=%d", *pdv_description, pdv->no_warning); } if (pdv->no_failed > 0) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s F=%d", *pdv_description, pdv->no_failed); } if (!pdv->is_pending && pdv->status) { *pdv_description = wmem_strdup_printf(pinfo->pool, "%s (%s)", *pdv_description, pdv->status); } } } } if (have_tap_listener(dicom_eo_tap)) { if (pdv->data_len == 0) { /* Copy pure DICOM data to buffer, without PDV flags Packet scope for the memory allocation is too small, since we may have PDV in different tvb. Therefore check if this was already done. */ pdv->data = wmem_alloc0(wmem_file_scope(), pdv_body_len); pdv->data_len = pdv_body_len; tvb_memcpy(tvb, pdv->data, startpos, pdv_body_len); } if ((pdv_body_len > 0) && (pdv->is_last_fragment)) { /* At the last segment, merge all related previous PDVs and copy to export buffer */ dcm_export_create_object(pinfo, assoc, pdv); } } return endpos; } /* Handle one PDV inside a data PDU. When needed, perform the reassembly of PDV fragments. PDV fragments are different from TCP fragmentation. Create PDV object when needed. Return pdv_description to be used e.g. in COL_INFO. */ static uint32_t dissect_dcm_pdv_fragmented(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_assoc_t *assoc, uint32_t offset, uint32_t pdv_len, char **pdv_description) { conversation_t *conv = NULL; dcm_state_pdv_t *pdv = NULL; tvbuff_t *combined_tvb = NULL; fragment_head *head = NULL; uint32_t reassembly_id; uint32_t pdv_body_len; pdv_body_len = pdv_len-2; /* Dissect Context ID, Find PDV object, Decode Command/Data flag and More Fragments flag */ offset = dissect_dcm_pdv_header(tvb, pinfo, tree, assoc, offset, &pdv); if (global_dcm_reassemble) { /* Combine the different PDVs. This is the default preference and useful in most scenarios. This will create one 'huge' PDV. E.g. a CT image will fits in one buffer. */ conv = find_conversation_pinfo(pinfo, 0); /* Try to create somewhat unique ID. Include the conversation index, to separate TCP session Include bits from the reassembly number in the current Presentation Context (that we track ourselves) in order to distinguish between PDV fragments from the same frame but different reassemblies. */ DISSECTOR_ASSERT(conv); /* The following expression seems to executed late in VS2017 in 'RelWithDebInf'. Therefore it may appear as 0 at first */ reassembly_id = (((conv->conv_index) & 0x000FFFFF) << 12) + ((uint32_t)(pdv->pctx_id) << 4) + ((uint32_t)(pdv->reassembly_id & 0xF)); /* This one will chain the packets until 'is_last_fragment' */ head = fragment_add_seq_next( &dcm_pdv_reassembly_table, tvb, offset, pinfo, reassembly_id, NULL, pdv_body_len, !(pdv->is_last_fragment)); if (head && (head->next == NULL)) { /* Was not really fragmented, therefore use 'conventional' decoding. process_reassembled_data() does not cope with two PDVs in the same frame, therefore catch it here */ offset = dissect_dcm_pdv_body(tvb, pinfo, tree, assoc, pdv, offset, pdv_body_len, pdv_description); } else { /* Will return a complete buffer, once last fragment is hit. The description is not used in packet-dcm. COL_INFO is set specifically in dissect_dcm_pdu() */ combined_tvb = process_reassembled_data( tvb, offset, pinfo, "Reassembled PDV", head, &dcm_pdv_fragment_items, NULL, tree); if (combined_tvb == NULL) { /* Just show this as a fragment */ if (head && head->reassembled_in != pinfo->num) { if (pdv->desc) { /* We know the presentation context already */ *pdv_description = wmem_strdup_printf(pinfo->pool, "%s (reassembled in #%u)", pdv->desc, head->reassembled_in); } else { /* Decoding of the presentation context did not occur yet or did not succeed */ *pdv_description = wmem_strdup_printf(pinfo->pool, "PDV Fragment (reassembled in #%u)", head->reassembled_in); } } else { /* We don't know the last fragment yet (and/or we'll never see it). This can happen, e.g. when TCP packet arrive our of order. */ *pdv_description = wmem_strdup(pinfo->pool, "PDV Fragment"); } offset += pdv_body_len; } else { /* Decode reassembled data. This needs to be += */ offset += dissect_dcm_pdv_body(combined_tvb, pinfo, tree, assoc, pdv, 0, tvb_captured_length(combined_tvb), pdv_description); } } } else { /* Do not reassemble DICOM PDVs, i.e. decode PDVs one by one. This may be useful when troubleshooting PDU length issues, or to better understand the PDV split. The tag level decoding is more challenging, as leftovers need to be displayed adequately. Not a big deal for binary values. */ offset = dissect_dcm_pdv_body(tvb, pinfo, tree, assoc, pdv, offset, pdv_body_len, pdv_description); } return offset; } static uint32_t dissect_dcm_pdu_data(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, dcm_state_assoc_t *assoc, uint32_t offset, uint32_t pdu_len, char **pdu_data_description) { /* 04 P-DATA-TF 1 1 reserved 2 4 length - (1+) presentation data value (PDV) items 6 4 length 10 1 Presentation Context ID (odd ints 1 - 255) - PDV 11 1 header 0x01 if set, contains Message Command info, else Message Data 0x02 if set, contains last fragment */ proto_tree *pdv_ptree; /* Tree for item details */ proto_item *pdv_pitem, *pdvlen_item; char *buf_desc = NULL; /* PDU description */ char *pdv_description = NULL; bool first_pdv = true; uint32_t endpos = 0; uint32_t pdv_len = 0; endpos = offset + pdu_len; /* Loop through multiple PDVs */ while (offset < endpos) { pdv_len = tvb_get_ntohl(tvb, offset); pdv_ptree = proto_tree_add_subtree(tree, tvb, offset, pdv_len+4, ett_dcm_data_pdv, &pdv_pitem, "PDV"); pdvlen_item = proto_tree_add_item(pdv_ptree, hf_dcm_pdv_len, tvb, offset, 4, ENC_BIG_ENDIAN); offset += 4; if ((pdv_len + 4 > pdu_len) || (pdv_len + 4 < pdv_len)) { expert_add_info_format(pinfo, pdvlen_item, &ei_dcm_pdv_len, "Invalid PDV length (too large)"); return endpos; } else if (pdv_len <= 2) { expert_add_info_format(pinfo, pdvlen_item, &ei_dcm_pdv_len, "Invalid PDV length (too small)"); return endpos; } else if (((pdv_len >> 1) << 1) != pdv_len) { expert_add_info_format(pinfo, pdvlen_item, &ei_dcm_pdv_len, "Invalid PDV length (not even)"); return endpos; } offset = dissect_dcm_pdv_fragmented(tvb, pinfo, pdv_ptree, assoc, offset, pdv_len, &pdv_description); /* The following doesn't seem to work anymore */ if (pdv_description) { if (first_pdv) { buf_desc = wmem_strdup(pinfo->pool, pdv_description); } else { buf_desc = wmem_strdup_printf(pinfo->pool, "%s, %s", buf_desc, pdv_description); } } proto_item_append_text(pdv_pitem, ", %s", pdv_description); first_pdv=false; } *pdu_data_description = buf_desc; return offset; } /* Test for DICOM traffic. - Minimum 10 Bytes - Look for the association request - Check PDU size vs TCP payload size Since used in heuristic mode, be picky for performance reasons. We are called in static mode, once we decoded the association request and called conversation_set_dissector() They we can be more liberal on the packet selection */ static bool test_dcm(tvbuff_t *tvb) { uint8_t pdu_type; uint32_t pdu_len; uint16_t vers; /* Ensure that the tvb_captured_length is big enough before fetching the values. Otherwise it can trigger an exception during the heuristic check, preventing next heuristic dissectors from being called tvb_reported_length() is the real size of the packet as transmitted on the wire tvb_captured_length() is the number of bytes captured (so you always have captured <= reported). The 10 bytes represent an association request header including the 2 reserved bytes not used below In the captures at hand, the parsing result was equal. */ if (tvb_captured_length(tvb) < 8) { return false; } if (tvb_reported_length(tvb) < 10) { return false; } pdu_type = tvb_get_uint8(tvb, 0); pdu_len = tvb_get_ntohl(tvb, 2); vers = tvb_get_ntohs(tvb, 6); /* Exit, if not an association request at version 1 */ if (!(pdu_type == 1 && vers == 1)) { return false; } /* Exit if TCP payload is bigger than PDU length (plus header) OK for PRESENTATION_DATA, questionable for ASSOCIATION requests */ if (tvb_reported_length(tvb) > pdu_len + 6) { return false; } return true; } /* Main function to decode DICOM traffic. Supports reassembly of TCP packets. */ static int dissect_dcm_main(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, bool is_port_static) { uint8_t pdu_type = 0; uint32_t pdu_start = 0; uint32_t pdu_len = 0; uint32_t tlen = 0; int offset = 0; /* TCP packets are assembled well by wireshark in conjunction with the dissectors. Therefore, we will only see properly aligned PDUs, at the beginning of the buffer. So if the buffer does not start with the PDU header, it's not DICOM traffic. Do the byte checking as early as possible. The heuristic hook requires an association request DICOM PDU are nice, but need to be managed We can have any combination: - One or more DICOM PDU per TCP packet - PDU split over different TCP packets - And both together, i.e. some complete PDUs and then a fraction of a new PDU in a TCP packet This function will handle multiple PDUs per TCP packet and will ask for more data, if the last PDU does not fit It does not reassemble fragmented PDVs by purpose, since the Tag Value parsing needs to be done per Tag, and PDU recombination here would a) need to eliminate PDU/PDV/Ctx header (12 bytes) b) not show the true DICOM logic in transfer The length check is tricky. If not a PDV continuation, 10 Bytes are required. For PDV continuation anything seems to be possible, depending on the buffer alignment of the sending process. */ tlen = tvb_reported_length(tvb); pdu_type = tvb_get_uint8(tvb, 0); if (pdu_type == 0 || pdu_type > 7) /* Wrong PDU type. 'Or' is slightly more efficient than 'and' */ return 0; /* No bytes taken from the stack */ if (is_port_static) { /* Port is defined explicitly, or association request was previously found successfully. Be more tolerant on minimum packet size. Also accept < 6 */ if (tlen < 6) { /* we need 6 bytes at least to get PDU length */ pinfo->desegment_offset = offset; pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT; return tvb_captured_length(tvb); } } /* Passing this point, we should always have tlen >= 6 */ pdu_len = tvb_get_ntohl(tvb, 2); if (pdu_len < 4) /* The smallest PDUs are ASSOC Rejects & Release messages */ return 0; /* Mark it. This is a DICOM packet */ col_set_str(pinfo->cinfo, COL_PROTOCOL, "DICOM"); /* Process all PDUs in the buffer */ while (pdu_start < tlen) { uint32_t old_pdu_start; if ((pdu_len+6) > (tlen-offset)) { /* PDU is larger than the remaining packet (buffer), therefore request whole PDU The next time this function is called, tlen will be equal to pdu_len */ pinfo->desegment_offset = offset; pinfo->desegment_len = (pdu_len+6) - (tlen-offset); return tvb_captured_length(tvb); } /* Process a whole PDU */ offset=dissect_dcm_pdu(tvb, pinfo, tree, pdu_start); /* Next PDU */ old_pdu_start = pdu_start; pdu_start = pdu_start + pdu_len + 6; if (pdu_start <= old_pdu_start) { expert_add_info_format(pinfo, NULL, &ei_dcm_invalid_pdu_length, "Invalid PDU length (%u)", pdu_len); break; } if (pdu_start < tlen - 6) { /* we got at least 6 bytes of the next PDU still in the buffer */ pdu_len = tvb_get_ntohl(tvb, pdu_start+2); } else { pdu_len = 0; } } return offset; } /* Callback function used to register */ static int dissect_dcm_static(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) { /* Less checking on ports that match */ return dissect_dcm_main(tvb, pinfo, tree, true); } /* Test for an Association Request. Decode, when successful. */ static bool dissect_dcm_heuristic(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) { /* This will be potentially called for every packet */ if (!test_dcm(tvb)) return false; /* Conversation_set_dissector() is called inside dcm_state_get() once we have enough details. From there on, we will be 'static' */ if (dissect_dcm_main(tvb, pinfo, tree, false) == 0) { /* there may have been another reason why it is not DICOM */ return false; } return true; } /* Only set a valued with col_set_str() if it does not yet exist. (In a multiple PDV scenario, col_set_str() actually appends for the subsequent calls) */ static void col_set_str_conditional(column_info *cinfo, const int el, const char* str) { const char *col_string = col_get_text(cinfo, el); if (col_string == NULL || !g_str_has_prefix(col_string, str)) { col_add_str(cinfo, el, str); } } /* CSV add a value to a column, if it does not exist yet */ static void col_append_str_conditional(column_info *cinfo, const int el, const char* str) { const char *col_string = col_get_text(cinfo, el); if (col_string == NULL || !g_strrstr(col_string, str)) { col_append_fstr(cinfo, el, ", %s", str); } } /* Dissect a single DICOM PDU. Can be an association or a data package. Creates a tree item. */ static uint32_t dissect_dcm_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint32_t offset) { proto_tree *dcm_ptree=NULL; /* Root DICOM tree and its item */ proto_item *dcm_pitem=NULL; dcm_state_t *dcm_data=NULL; dcm_state_assoc_t *assoc=NULL; uint8_t pdu_type=0; uint32_t pdu_len=0; char *pdu_data_description=NULL; /* Get or create conversation. Used to store context IDs and xfer Syntax */ dcm_data = dcm_state_get(pinfo, true); if (dcm_data == NULL) { /* Internal error. Failed to create main DICOM data structure */ return offset; } dcm_pitem = proto_tree_add_item(tree, proto_dcm, tvb, offset, -1, ENC_NA); dcm_ptree = proto_item_add_subtree(dcm_pitem, ett_dcm); /* PDU type is only one byte, then one byte reserved */ pdu_type = tvb_get_uint8(tvb, offset); proto_tree_add_item(dcm_ptree, hf_dcm_pdu_type, tvb, offset, 1, ENC_BIG_ENDIAN); offset += 2; pdu_len = tvb_get_ntohl(tvb, offset); proto_tree_add_item(dcm_ptree, hf_dcm_pdu_len, tvb, offset, 4, ENC_BIG_ENDIAN); offset += 4; /* Find previously detected association, else create a new one object*/ assoc = dcm_state_assoc_get(dcm_data, pinfo->num, true); if (assoc == NULL) { /* Internal error. Failed to create association structure */ return offset; } if (pdu_type == 4) { col_set_str_conditional(pinfo->cinfo, COL_INFO, "P-DATA"); /* Everything that needs to be shown in any UI column (like COL_INFO) needs to be calculated also with tree == null */ offset = dissect_dcm_pdu_data(tvb, pinfo, dcm_ptree, assoc, offset, pdu_len, &pdu_data_description); if (pdu_data_description) { proto_item_append_text(dcm_pitem, ", %s", pdu_data_description); col_append_str_conditional(pinfo->cinfo, COL_INFO, pdu_data_description); } } else { /* Decode Association request, response, reject, abort details */ offset = dissect_dcm_assoc_header(tvb, pinfo, dcm_ptree, offset, assoc, pdu_type, pdu_len); } return offset; /* return the number of processed bytes */ } /* Register the protocol with Wireshark */ void proto_register_dcm(void) { static hf_register_info hf[] = { { &hf_dcm_pdu_type, { "PDU Type", "dicom.pdu.type", FT_UINT8, BASE_HEX, VALS(dcm_pdu_ids), 0, NULL, HFILL } }, { &hf_dcm_pdu_len, { "PDU Length", "dicom.pdu.len", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_version, { "Protocol Version", "dicom.assoc.version", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_called, { "Called AE Title", "dicom.assoc.ae.called", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_calling, { "Calling AE Title", "dicom.assoc.ae.calling", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_reject_result, { "Result", "dicom.assoc.reject.result", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_reject_source, { "Source", "dicom.assoc.reject.source", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_reject_reason, { "Reason", "dicom.assoc.reject.reason", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_abort_source, { "Source", "dicom.assoc.abort.source", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_abort_reason, { "Reason", "dicom.assoc.abort.reason", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_item_type, { "Item Type", "dicom.assoc.item.type", FT_UINT8, BASE_HEX, VALS(dcm_assoc_item_type), 0, NULL, HFILL } }, { &hf_dcm_assoc_item_len, { "Item Length", "dicom.assoc.item.len", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_actx, { "Application Context", "dicom.actx", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_pctx_id, { "Presentation Context ID", "dicom.pctx.id", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL } }, { &hf_dcm_pctx_result, { "Presentation Context Result", "dicom.pctx.result", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL } }, { &hf_dcm_pctx_abss_syntax, { "Abstract Syntax", "dicom.pctx.abss.syntax", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_pctx_xfer_syntax, { "Transfer Syntax", "dicom.pctx.xfer.syntax", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info, { "User Info", "dicom.userinfo", FT_NONE, BASE_NONE, NULL, 0, "This field contains the ACSE User Information Item of the A-ASSOCIATErequest.", HFILL } }, { &hf_dcm_info_uid, { "Implementation Class UID", "dicom.userinfo.uid", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_version, { "Implementation Version", "dicom.userinfo.version", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_extneg, { "Extended Negotiation", "dicom.userinfo.extneg", FT_NONE, BASE_NONE, NULL, 0, "This field contains the optional SOP Class Extended Negotiation Sub-Item of the ACSE User Information Item of the A-ASSOCIATE-RQ/RSP.", HFILL } }, { &hf_dcm_info_extneg_sopclassuid_len, { "SOP Class UID Length", "dicom.userinfo.extneg.sopclassuid.len", FT_UINT16, BASE_DEC, NULL, 0, "This field contains the length of the SOP Class UID in the Extended Negotiation Sub-Item.", HFILL } }, { &hf_dcm_info_extneg_sopclassuid, { "SOP Class UID", "dicom.userinfo.extneg.sopclassuid", FT_STRING, BASE_NONE, NULL, 0, "This field contains the SOP Class UID in the Extended Negotiation Sub-Item.", HFILL } }, { &hf_dcm_info_extneg_relational_query, { "Relational-queries", "dicom.userinfo.extneg.relational", FT_UINT8, BASE_HEX, NULL, 0, "This field indicates, if relational queries are supported.", HFILL } }, { &hf_dcm_info_extneg_date_time_matching, { "Combined Date-Time matching", "dicom.userinfo.extneg.datetimematching", FT_UINT8, BASE_HEX, NULL, 0, "This field indicates, if combined date-time matching is supported.", HFILL } }, { &hf_dcm_info_extneg_fuzzy_semantic_matching, { "Fuzzy semantic matching", "dicom.userinfo.extneg.fuzzymatching", FT_UINT8, BASE_HEX, NULL, 0, "This field indicates, if fuzzy semantic matching of person names is supported.", HFILL } }, { &hf_dcm_info_extneg_timezone_query_adjustment, { "Timezone query adjustment", "dicom.userinfo.extneg.timezone", FT_UINT8, BASE_HEX, NULL, 0, "This field indicates, if timezone query adjustment is supported.", HFILL } }, { &hf_dcm_info_rolesel, { "SCP/SCU Role Selection", "dicom.userinfo.rolesel", FT_NONE, BASE_NONE, NULL, 0, "This field contains the optional SCP/SCU Role Selection Sub-Item of the ACSE User Information Item of the A-ASSOCIATE-RQ/RSP.", HFILL } }, { &hf_dcm_info_rolesel_sopclassuid_len, { "SOP Class UID Length", "dicom.userinfo.rolesel.sopclassuid.len", FT_UINT16, BASE_DEC, NULL, 0, "This field contains the length of the SOP Class UID in the SCP/SCU Role Selection Sub-Item.", HFILL } }, { &hf_dcm_info_rolesel_sopclassuid, { "SOP Class UID", "dicom.userinfo.rolesel.sopclassuid", FT_STRING, BASE_NONE, NULL, 0, "This field contains the SOP Class UID in the SCP/SCU Role Selection Sub-Item.", HFILL } }, { &hf_dcm_info_rolesel_scurole, { "SCU-role", "dicom.userinfo.rolesel.scurole", FT_UINT8, BASE_HEX, NULL, 0, "This field contains the SCU-role as defined for the Association-requester.", HFILL } }, { &hf_dcm_info_rolesel_scprole, { "SCP-role", "dicom.userinfo.rolesel.scprole", FT_UINT8, BASE_HEX, NULL, 0, "This field contains the SCP-role as defined for the Association-requester.", HFILL } }, { &hf_dcm_info_async_neg, { "Asynchronous Operations (and sub-operations) Window Negotiation", "dicom.userinfo.asyncneg", FT_NONE, BASE_NONE, NULL, 0, "This field contains the optional Asynchronous Operations (and sub-operations) Window Negotiation Sub-Item of the ACSE User Information Item of the A-ASSOCIATE-RQ/RSP.", HFILL } }, { &hf_dcm_info_async_neg_max_num_ops_inv, { "Maximum-number-operations-invoked", "dicom.userinfo.asyncneg.maxnumopsinv", FT_UINT16, BASE_DEC, NULL, 0, "This field contains the maximum-number-operations-invoked in the Asynchronous Operations (and sub-operations) Window Negotiation Sub-Item.", HFILL } }, { &hf_dcm_info_async_neg_max_num_ops_per, { "Maximum-number-operations-performed", "dicom.userinfo.asyncneg.maxnumopsper", FT_UINT16, BASE_DEC, NULL, 0, "This field contains the maximum-number-operations-performed in the Asynchronous Operations (and sub-operations) Window Negotiation Sub-Item.", HFILL } }, { &hf_dcm_info_unknown, { "Unknown", "dicom.userinfo.unknown", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_assoc_item_data, { "Unknown Data", "dicom.userinfo.data", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify, { "User Identify", "dicom.userinfo.user_identify", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_type, { "Type", "dicom.userinfo.user_identify.type", FT_UINT8, BASE_DEC, VALS(user_identify_type_vals), 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_response_requested, { "Response Requested", "dicom.userinfo.user_identify.response_requested", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_primary_field_length, { "Primary Field Length", "dicom.userinfo.user_identify.primary_field_length", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_primary_field, { "Primary Field", "dicom.userinfo.user_identify.primary_field", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_secondary_field_length, { "Secondary Field Length", "dicom.userinfo.user_identify.secondary_field_length", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_info_user_identify_secondary_field, { "Secondary Field", "dicom.userinfo.user_identify.secondary_field", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_pdu_maxlen, { "Max PDU Length", "dicom.max_pdu_len", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_pdv_len, { "PDV Length", "dicom.pdv.len", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_pdv_ctx, { "PDV Context", "dicom.pdv.ctx", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_pdv_flags, { "PDV Flags", "dicom.pdv.flags", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL } }, { &hf_dcm_data_tag, { "Tag", "dicom.data.tag", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag, { "Tag", "dicom.tag", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_vr, { "VR", "dicom.tag.vr", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_vl, { "Length", "dicom.tag.vl", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_str, { "Value", "dicom.tag.value.str", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_16s, { "Value", "dicom.tag.value.16s", FT_INT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_16u, { "Value", "dicom.tag.value.16u", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_32s, { "Value", "dicom.tag.value.32s", FT_INT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_32u, { "Value", "dicom.tag.value.32u", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL } }, { &hf_dcm_tag_value_byte, { "Value", "dicom.tag.value.byte", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL } }, /* Fragment entries */ { &hf_dcm_pdv_fragments, { "Message fragments", "dicom.pdv.fragments", FT_NONE, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment, { "Message fragment", "dicom.pdv.fragment", FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_overlap, { "Message fragment overlap", "dicom.pdv.fragment.overlap", FT_BOOLEAN, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_overlap_conflicts, { "Message fragment overlapping with conflicting data", "dicom.pdv.fragment.overlap.conflicts", FT_BOOLEAN, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_multiple_tails, { "Message has multiple tail fragments", "dicom.pdv.fragment.multiple_tails", FT_BOOLEAN, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_too_long_fragment, { "Message fragment too long", "dicom.pdv.fragment.too_long_fragment", FT_BOOLEAN, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_error, { "Message defragmentation error", "dicom.pdv.fragment.error", FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_fragment_count, { "Message fragment count", "dicom.pdv.fragment_count", FT_UINT32, BASE_DEC, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_reassembled_in, { "Reassembled in", "dicom.pdv.reassembled.in", FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } }, { &hf_dcm_pdv_reassembled_length, { "Reassembled PDV length", "dicom.pdv.reassembled.length", FT_UINT32, BASE_DEC, NULL, 0x00, NULL, HFILL } } }; /* Setup protocol subtree array */ static int *ett[] = { &ett_dcm, &ett_assoc, &ett_assoc_header, &ett_assoc_actx, &ett_assoc_pctx, &ett_assoc_pctx_abss, &ett_assoc_pctx_xfer, &ett_assoc_info, &ett_assoc_info_uid, &ett_assoc_info_version, &ett_assoc_info_extneg, &ett_assoc_info_rolesel, &ett_assoc_info_async_neg, &ett_assoc_info_user_identify, &ett_assoc_info_unknown, &ett_dcm_data, &ett_dcm_data_pdv, &ett_dcm_data_tag, &ett_dcm_data_seq, &ett_dcm_data_item, &ett_dcm_pdv, /* used for fragments */ &ett_dcm_pdv_fragment, &ett_dcm_pdv_fragments }; static ei_register_info ei[] = { { &ei_dcm_assoc_rejected, { "dicom.assoc.reject", PI_RESPONSE_CODE, PI_WARN, "Association rejected", EXPFILL }}, { &ei_dcm_assoc_aborted, { "dicom.assoc.abort", PI_RESPONSE_CODE, PI_WARN, "Association aborted", EXPFILL }}, { &ei_dcm_no_abstract_syntax, { "dicom.no_abstract_syntax", PI_MALFORMED, PI_ERROR, "No Abstract Syntax provided for this Presentation Context", EXPFILL }}, { &ei_dcm_multiple_abstract_syntax, { "dicom.multiple_abstract_syntax", PI_MALFORMED, PI_ERROR, "More than one Abstract Syntax provided for this Presentation Context", EXPFILL }}, { &ei_dcm_no_transfer_syntax, { "dicom.no_transfer_syntax", PI_MALFORMED, PI_ERROR, "No Transfer Syntax provided for this Presentation Context", EXPFILL }}, { &ei_dcm_no_abstract_syntax_uid, { "dicom.no_abstract_syntax_uid", PI_MALFORMED, PI_ERROR, "No Abstract Syntax UID found for this Presentation Context", EXPFILL }}, { &ei_dcm_multiple_transfer_syntax, { "dicom.multiple_transfer_syntax", PI_MALFORMED, PI_ERROR, "Only one Transfer Syntax allowed in a Association Response", EXPFILL }}, { &ei_dcm_assoc_item_len, { "dicom.assoc.item.len.invalid", PI_MALFORMED, PI_ERROR, "Invalid Association Item Length", EXPFILL }}, { &ei_dcm_pdv_ctx, { "dicom.pdv.ctx.invalid", PI_MALFORMED, PI_ERROR, "Invalid Presentation Context ID", EXPFILL }}, { &ei_dcm_pdv_flags, { "dicom.pdv.flags.invalid", PI_MALFORMED, PI_ERROR, "Invalid Flags", EXPFILL }}, { &ei_dcm_status_msg, { "dicom.status_msg", PI_RESPONSE_CODE, PI_WARN, "%s", EXPFILL }}, { &ei_dcm_data_tag, { "dicom.data.tag.missing", PI_MALFORMED, PI_ERROR, "Early termination of tag. Data is missing", EXPFILL }}, { &ei_dcm_pdv_len, { "dicom.pdv.len.invalid", PI_MALFORMED, PI_ERROR, "Invalid PDV length", EXPFILL }}, { &ei_dcm_invalid_pdu_length, { "dicom.pdu_length.invalid", PI_MALFORMED, PI_ERROR, "Invalid PDU length", EXPFILL }}, }; module_t *dcm_module; expert_module_t* expert_dcm; /* Register the protocol name and description */ proto_dcm = proto_register_protocol("DICOM", "DICOM", "dicom"); /* Required function calls to register the header fields and subtrees used */ proto_register_field_array(proto_dcm, hf, array_length(hf)); proto_register_subtree_array(ett, array_length(ett)); expert_dcm = expert_register_protocol(proto_dcm); expert_register_field_array(expert_dcm, ei, array_length(ei)); /* Allow other dissectors to find this one by name. */ dcm_handle = register_dissector("dicom", dissect_dcm_static, proto_dcm); dcm_module = prefs_register_protocol(proto_dcm, NULL); /* Used to migrate an older configuration file to a newer one */ prefs_register_obsolete_preference(dcm_module, "heuristic"); prefs_register_bool_preference(dcm_module, "export_header", "Create Meta Header on Export", "Create DICOM File Meta Header according to PS 3.10 on export for PDUs. " "If the captured PDV does not contain a SOP Class UID and SOP Instance UID " "(e.g. for command PDVs), wireshark specific ones will be created.", &global_dcm_export_header); prefs_register_uint_preference(dcm_module, "export_minsize", "Min. item size in bytes to export", "Do not show items below this size in the export list. " "Set it to 0, to see DICOM commands and responses in the list. " "Set it higher, to just export DICOM IODs (i.e. CT Images, RT Structures).", 10, &global_dcm_export_minsize); prefs_register_bool_preference(dcm_module, "seq_tree", "Create subtrees for Sequences and Items", "Create a node for sequences and items, and show children in a hierarchy. " "De-select this option, if you prefer a flat display or e.g. " "when using TShark to create a text output.", &global_dcm_seq_subtree); prefs_register_bool_preference(dcm_module, "tag_tree", "Create subtrees for DICOM Tags", "Create a node for a tag and show tag details as single elements. " "This can be useful to debug a tag and to allow display filters on these attributes. " "When using TShark to create a text output, it's better to have it disabled. ", &global_dcm_tag_subtree); prefs_register_bool_preference(dcm_module, "cmd_details", "Show command details in header", "Show message ID and number of completed, remaining, warned or failed operations in header and info column.", &global_dcm_cmd_details); prefs_register_bool_preference(dcm_module, "pdv_reassemble", "Merge fragmented PDVs", "Decode all DICOM tags in the last PDV. This will ensure the proper reassembly. " "De-select, to troubleshoot PDU length issues, or to understand PDV fragmentation. " "When not set, the decoding may fail and the exports may become corrupt.", &global_dcm_reassemble); dicom_eo_tap = register_export_object(proto_dcm, dcm_eo_packet, NULL); register_init_routine(&dcm_init); /* Register processing of fragmented DICOM PDVs */ reassembly_table_register(&dcm_pdv_reassembly_table, &addresses_reassembly_table_functions); } /* Register static TCP port range specified in preferences. Register heuristic search as well. Statically defined ports take precedence over a heuristic one. I.e., if a foreign protocol claims a port, where DICOM is running on, we would never be called, by just having the heuristic registration. This function is also called, when preferences change. */ void proto_reg_handoff_dcm(void) { /* Adds a UI element to the preferences dialog. This is the static part. */ dissector_add_uint_range_with_preference("tcp.port", DICOM_DEFAULT_RANGE, dcm_handle); /* The following shows up as child protocol of 'DICOM' in 'Enable/Disable Protocols ...' The registration procedure for dissectors is a two-stage procedure. In stage 1, dissectors create tables in which other dissectors can register them. That's the stage in which proto_register_ routines are called. In stage 2, dissectors register themselves in tables created in stage 1. That's the stage in which proto_reg_handoff_ routines are called. heur_dissector_add() needs to be called in proto_reg_handoff_dcm() function. */ heur_dissector_add("tcp", dissect_dcm_heuristic, "DICOM on any TCP port (heuristic)", "dicom_tcp", proto_dcm, HEURISTIC_ENABLE); } /* PDU's 01 ASSOC-RQ 1 1 reserved 2 4 length 6 2 protocol version (0x0 0x1) 8 2 reserved 10 16 dest aetitle 26 16 src aetitle 42 32 reserved 74 - presentation data value items 02 A-ASSOC-AC 1 reserved 4 length 2 protocol version (0x0 0x1) 2 reserved 16 dest aetitle (not checked) 16 src aetitle (not checked) 32 reserved - presentation data value items 03 ASSOC-RJ 1 reserved 4 length (4) 1 reserved 1 result (1 reject perm, 2 reject transient) 1 source (1 service user, 2 service provider, 3 service provider) 1 reason 1 == source 1 no reason given 2 application context name not supported 3 calling aetitle not recognized 7 called aetitle not recognized 2 == source 1 no reason given 2 protocol version not supported 3 == source 1 temporary congestion 2 local limit exceeded 04 P-DATA 1 1 reserved 2 4 length - (1+) presentation data value (PDV) items 6 4 length 10 1 Presentation Context ID (odd ints 1 - 255) - PDV 11 1 header 0x01 if set, contains Message Command info, else Message Data 0x02 if set, contains last fragment 05 A-RELEASE-RQ 1 reserved 4 length (4) 4 reserved 06 A-RELEASE-RP 1 reserved 4 length (4) 4 reserved 07 A-ABORT 1 reserved 4 length (4) 2 reserved 1 source (0 = user, 1 = provider) 1 reason if 1 == source (0 not spec, 1 unrecognized, 2 unexpected 4 unrecognized param, 5 unexpected param, 6 invalid param) ITEM's 10 Application Context 1 reserved 2 length - name 20 Presentation Context 1 reserved 2 length 1 Presentation context id 3 reserved - (1) abstract and (1+) transfer syntax sub-items 21 Presentation Context (Reply) 1 reserved 2 length 1 ID (odd int's 1-255) 1 reserved 1 result (0 accept, 1 user-reject, 2 no-reason, 3 abstract not supported, 4- transfer syntax not supported) 1 reserved - (1) type 40 30 Abstract syntax 1 reserved 2 length - name (<= 64) 40 Transfer syntax 1 reserved 2 length - name (<= 64) 50 user information 1 reserved 2 length - user data 51 max length 1 reserved 2 length (4) 4 max PDU lengths From 3.7 Annex D Association Negotiation ======================================== 52 IMPLEMENTATION CLASS UID 1 Item-type 52H 1 Reserved 2 Item-length n Implementation-class-uid 55 IMPLEMENTATION VERSION NAME 1 Item-type 55H 1 Reserved 2 Item-length n Implementation-version-name 53 ASYNCHRONOUS OPERATIONS WINDOW 1 Item-type 53H 1 Reserved 2 Item-length 2 Maximum-number-operations-invoked 2 Maximum-number-operations-performed 54 SCP/SCU ROLE SELECTION 1 Item-type 54H 1 Reserved 2 Item-length (n) 2 UID-length (m) m SOP-class-uid 1 SCU-role 0 - non support of the SCU role 1 - support of the SCU role 1 SCP-role 0 - non support of the SCP role 1 - support of the SCP role. 56 SOP CLASS EXTENDED NEGOTIATION 1 Item-type 56H 1 Reserved 2 Item-Length (n) 2 SOP-class-uid-length (m) m SOP-class-uid n-m Service-class-application-information 57 SOP CLASS COMMON EXTENDED NEGOTIATION 1 Item-type 57H 1 Sub-item-version 2 Item-Length 2 SOP-class-uid-length (m) 7-x SOP-class-uid The SOP Class identifier encoded as a UID as defined in PS 3.5. (x+1)-(x+2) Service-class-uid-length The Service-class-uid-length shall be the number of bytes in the Service-class-uid field. It shall be encoded as an unsigned binary number. (x+3)-y Service-class-uid The Service Class identifier encoded as a UID as defined in PS 3.5. (y+1)-(y+2) Related-general-sop-class-identification-length The Related-general-sop-class-identification-length shall be the number of bytes in the Related-general-sop-class-identification field. Shall be zero if no Related General SOP Classes are identified. (y+3)-z Related-general-sop-class-identification The Related-general-sop-class-identification is a sequence of pairs of length and UID sub-fields. Each pair of sub-fields shall be formatted in accordance with Table D.3-13. (z+1)-k Reserved Reserved for additional fields of the sub-item. Shall be zero-length for Version 0 of Sub-item definition. Table D.3-13 RELATED-GENERAL-SOP-CLASS-IDENTIFICATION SUB-FIELDS Bytes Sub-Field Name Description of Sub-Field 1-2 Related-general-sop-class-uid-length The Related-general-sop-class-uid-length shall be the number of bytes in the Related-general-sop-class-uid sub-field. It shall be encoded as an unsigned binary number. 3-n Related-general-sop-class-uid The Related General SOP Class identifier encoded as a UID as defined in PS 3.5. 58 User Identity Negotiation 1 Item-type 58H 1 Reserved 2 Item-length 1 User-Identity-Type Field value shall be in the range 1 to 4 with the following meanings: 1 - Username as a string in UTF-8 2 - Username as a string in UTF-8 and passcode 3 - Kerberos Service ticket 4 - SAML Assertion Other values are reserved for future standardization. 1 Positive-response-requested Field value: 0 - no response requested 1 - positive response requested 2 Primary-field-length The User-Identity-Length shall contain the length of the User-Identity value. 9-n Primary-field This field shall convey the user identity, either the username as a series of characters, or the Kerberos Service ticket encoded in accordance with RFC-1510. n+1-n+2 Secondary-field-length This field shall be non-zero only if User-Identity-Type has the value 2. It shall contain the length of the secondary-field. n+3-m Secondary-field This field shall be present only if User-Identity-Type has the value 2. It shall contain the Passcode value. 59 User Identity Negotiation Reply 1 Item-type 59H 1 Reserved 2 Item-length 5-6 Server-response-length This field shall contain the number of bytes in the Server-response. May be zero. 7-n Server-response This field shall contain the Kerberos Server ticket, encoded in accordance with RFC-1510, if the User-Identity-Type value in the A-ASSOCIATE-RQ was 3. This field shall contain the SAML response if the User-Identity-Type value in the A-ASSOCIATE-RQ was 4. This field shall be zero length if the value of the User-Identity-Type in the A-ASSOCIATE-RQ was 1 or 2. */ /* * Editor modelines - https://www.wireshark.org/tools/modelines.html * * Local variables: * c-basic-offset: 4 * tab-width: 8 * indent-tabs-mode: nil * End: * * vi: set shiftwidth=4 tabstop=8 expandtab: * :indentSize=4:tabSize=8:noTabs=true: */