Skip to main content

dicom_ul/pdu/
mod.rs

1//! Protocol Data Unit module
2//!
3//! This module comprises multiple data structures representing possible
4//! protocol data units (PDUs) according to
5//! the standard message exchange mechanisms,
6//! as well as readers and writers of PDUs from arbitrary data sources.
7pub mod reader;
8pub mod writer;
9
10use std::fmt::Display;
11
12pub use reader::read_pdu;
13use snafu::{Backtrace, Snafu};
14pub use writer::{write_pdu, WriteChunkError};
15
16/// The length of the PDU header in bytes,
17/// comprising the PDU type (1 byte),
18/// reserved byte (1 byte),
19/// and PDU length (4 bytes).
20pub const PDU_HEADER_SIZE: u32 = 6;
21
22/// The length of a PDV header + message control header in bytes,
23/// comprising the PDV length (4 bytes),
24/// presentation context ID (1 byte),
25/// and the flags that precede the data (1 byte).
26pub const PDV_HEADER_SIZE: u32 = 6;
27
28/// The default maximum PDU size
29pub const DEFAULT_MAX_PDU: u32 = 16_384 - PDU_HEADER_SIZE;
30
31/// The smallest maximum PDU length negotiable
32/// by this implementation
33pub const MINIMUM_PDU_SIZE: u32 = 1_024 - PDU_HEADER_SIZE;
34
35/// A generous PDU size for internal use.
36/// Prefer to initialize buffers no larger than this size
37pub(crate) const LARGE_PDU_SIZE: u32 = 262_144 - PDU_HEADER_SIZE;
38
39#[derive(Debug, Snafu)]
40#[non_exhaustive]
41pub enum WriteError {
42    #[snafu(display("Could not write chunk of {} PDU structure", name))]
43    WriteChunk {
44        /// the name of the PDU structure
45        name: &'static str,
46        source: WriteChunkError,
47    },
48
49    #[snafu(display("Could not write field `{}`", field))]
50    WriteField {
51        field: &'static str,
52        backtrace: Backtrace,
53        source: std::io::Error,
54    },
55
56    #[snafu(display("Could not write {} reserved bytes", bytes))]
57    WriteReserved {
58        bytes: u32,
59        backtrace: Backtrace,
60        source: std::io::Error,
61    },
62
63    #[snafu(display("Could not write field `{}`", field))]
64    EncodeField {
65        field: &'static str,
66        #[snafu(backtrace)]
67        source: dicom_encoding::text::EncodeTextError,
68    },
69}
70
71#[derive(Debug, Snafu)]
72#[non_exhaustive]
73pub enum ReadError {
74    #[snafu(display("Max PDU length {max_pdu_length} is not acceptable"))]
75    InvalidMaxPdu {
76        max_pdu_length: u32,
77        backtrace: Backtrace,
78    },
79
80    #[snafu(display("No PDU available"))]
81    NoPduAvailable { backtrace: Backtrace },
82
83    #[snafu(display("Could not read PDU"), visibility(pub(crate)))]
84    ReadPdu {
85        source: std::io::Error,
86        backtrace: Backtrace,
87    },
88
89    #[snafu(display("Could not read PDU item"))]
90    ReadPduItem {
91        source: std::io::Error,
92        backtrace: Backtrace,
93    },
94
95    #[snafu(display("Could not read PDU field `{}`", field))]
96    ReadPduField {
97        field: &'static str,
98        source: std::io::Error,
99        backtrace: Backtrace,
100    },
101
102    #[snafu(display("Invalid item length {} (must be >=2)", length))]
103    InvalidItemLength { length: u32 },
104
105    #[snafu(display("Could not read {} reserved bytes", bytes))]
106    ReadReserved {
107        bytes: u32,
108        source: std::io::Error,
109        backtrace: Backtrace,
110    },
111
112    #[snafu(display(
113        "Incoming pdu was too large: length {}, maximum is {}",
114        pdu_length,
115        max_pdu_length
116    ))]
117    PduTooLarge {
118        pdu_length: u32,
119        max_pdu_length: u32,
120        backtrace: Backtrace,
121    },
122    #[snafu(display("PDU contained an invalid value {:?}", var_item))]
123    InvalidPduVariable {
124        var_item: PduVariableItem,
125        backtrace: Backtrace,
126    },
127    #[snafu(display("Multiple transfer syntaxes were accepted"))]
128    MultipleTransferSyntaxesAccepted { backtrace: Backtrace },
129    #[snafu(display("Invalid reject source or reason"))]
130    InvalidRejectSourceOrReason { backtrace: Backtrace },
131    #[snafu(display("Invalid abort service provider"))]
132    InvalidAbortSourceOrReason { backtrace: Backtrace },
133    #[snafu(display("Invalid presentation context result reason"))]
134    InvalidPresentationContextResultReason { backtrace: Backtrace },
135    #[snafu(display("invalid transfer syntax sub-item"))]
136    InvalidTransferSyntaxSubItem { backtrace: Backtrace },
137    #[snafu(display("unknown presentation context sub-item"))]
138    UnknownPresentationContextSubItem { backtrace: Backtrace },
139    #[snafu(display("Could not decode text field `{}`", field))]
140    DecodeText {
141        field: &'static str,
142        #[snafu(backtrace)]
143        source: dicom_encoding::text::DecodeTextError,
144    },
145    #[snafu(display("Missing application context name"))]
146    MissingApplicationContextName { backtrace: Backtrace },
147    #[snafu(display("Missing abstract syntax"))]
148    MissingAbstractSyntax { backtrace: Backtrace },
149    #[snafu(display("Missing transfer syntax"))]
150    MissingTransferSyntax { backtrace: Backtrace },
151    #[snafu(display("Invalid PDU field length"))]
152    InvalidPduFieldLength { backtrace: Backtrace },
153    #[snafu(display("Could not read user variable"))]
154    ReadUserVariable { backtrace: Backtrace },
155}
156
157/// Message component for a proposed presentation context.
158#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
159pub struct PresentationContextProposed {
160    /// the presentation context identifier
161    pub id: u8,
162    /// the expected abstract syntax UID
163    /// (commonly referrering to the expected SOP class)
164    pub abstract_syntax: String,
165    /// a list of transfer syntax UIDs to support in this interaction
166    pub transfer_syntaxes: Vec<String>,
167}
168
169/// Message component for the result of the presentation context negotiation.
170#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
171pub struct PresentationContextResult {
172    pub id: u8,
173    pub reason: PresentationContextResultReason,
174    pub transfer_syntax: String,
175}
176
177/// Result of the presentation context negotiation, including the
178/// abstract syntax that corresponds to each presentation context.
179#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
180pub struct PresentationContextNegotiated {
181    pub id: u8,
182    pub reason: PresentationContextResultReason,
183    pub transfer_syntax: String,
184    pub abstract_syntax: String,
185}
186
187#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
188pub enum PresentationContextResultReason {
189    Acceptance = 0,
190    UserRejection = 1,
191    NoReason = 2,
192    AbstractSyntaxNotSupported = 3,
193    TransferSyntaxesNotSupported = 4,
194}
195
196impl PresentationContextResultReason {
197    fn from(reason: u8) -> Option<PresentationContextResultReason> {
198        let result = match reason {
199            0 => PresentationContextResultReason::Acceptance,
200            1 => PresentationContextResultReason::UserRejection,
201            2 => PresentationContextResultReason::NoReason,
202            3 => PresentationContextResultReason::AbstractSyntaxNotSupported,
203            4 => PresentationContextResultReason::TransferSyntaxesNotSupported,
204            _ => {
205                return None;
206            }
207        };
208
209        Some(result)
210    }
211}
212
213impl Display for PresentationContextResultReason {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        let msg = match self {
216            PresentationContextResultReason::Acceptance => "acceptance",
217            PresentationContextResultReason::UserRejection => "user rejection",
218            PresentationContextResultReason::NoReason => "no reason",
219            PresentationContextResultReason::AbstractSyntaxNotSupported => {
220                "abstract syntax not supported"
221            }
222            PresentationContextResultReason::TransferSyntaxesNotSupported => {
223                "transfer syntaxes not supported"
224            }
225        };
226        f.write_str(msg)
227    }
228}
229
230#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
231pub enum AssociationRJResult {
232    Permanent = 1,
233    Transient = 2,
234}
235
236impl AssociationRJResult {
237    fn from(value: u8) -> Option<AssociationRJResult> {
238        match value {
239            1 => Some(AssociationRJResult::Permanent),
240            2 => Some(AssociationRJResult::Transient),
241            _ => None,
242        }
243    }
244}
245
246#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
247pub enum AssociationRJSource {
248    ServiceUser(AssociationRJServiceUserReason),
249    ServiceProviderASCE(AssociationRJServiceProviderASCEReason),
250    ServiceProviderPresentation(AssociationRJServiceProviderPresentationReason),
251}
252
253impl AssociationRJSource {
254    fn from(source: u8, reason: u8) -> Option<AssociationRJSource> {
255        let result = match (source, reason) {
256            (1, 1) => {
257                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::NoReasonGiven)
258            }
259            (1, 2) => AssociationRJSource::ServiceUser(
260                AssociationRJServiceUserReason::ApplicationContextNameNotSupported,
261            ),
262            (1, 3) => AssociationRJSource::ServiceUser(
263                AssociationRJServiceUserReason::CallingAETitleNotRecognized,
264            ),
265            (1, x) if x == 4 || x == 5 || x == 6 => {
266                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::Reserved(x))
267            }
268            (1, 7) => AssociationRJSource::ServiceUser(
269                AssociationRJServiceUserReason::CalledAETitleNotRecognized,
270            ),
271            //(1, 8) | (1, 9) | (1, 10) => {
272            (1, x) if x == 8 || x == 9 || x == 10 => {
273                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::Reserved(x))
274            }
275            (1, _) => {
276                return None;
277            }
278            (2, 1) => AssociationRJSource::ServiceProviderASCE(
279                AssociationRJServiceProviderASCEReason::NoReasonGiven,
280            ),
281            (2, 2) => AssociationRJSource::ServiceProviderASCE(
282                AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported,
283            ),
284            (2, _) => {
285                return None;
286            }
287            (3, 0) => AssociationRJSource::ServiceProviderPresentation(
288                AssociationRJServiceProviderPresentationReason::Reserved(0),
289            ),
290            (3, 1) => AssociationRJSource::ServiceProviderPresentation(
291                AssociationRJServiceProviderPresentationReason::TemporaryCongestion,
292            ),
293            (3, 2) => AssociationRJSource::ServiceProviderPresentation(
294                AssociationRJServiceProviderPresentationReason::LocalLimitExceeded,
295            ),
296            (3, x) if x == 3 || x == 4 || x == 5 || x == 6 || x == 7 => {
297                AssociationRJSource::ServiceProviderPresentation(
298                    AssociationRJServiceProviderPresentationReason::Reserved(x),
299                )
300            }
301            _ => {
302                return None;
303            }
304        };
305        Some(result)
306    }
307}
308
309impl Display for AssociationRJSource {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        match self {
312            AssociationRJSource::ServiceUser(r) => Display::fmt(r, f),
313            AssociationRJSource::ServiceProviderASCE(r) => Display::fmt(r, f),
314            AssociationRJSource::ServiceProviderPresentation(r) => Display::fmt(r, f),
315        }
316    }
317}
318
319#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
320pub enum AssociationRJServiceUserReason {
321    NoReasonGiven,
322    ApplicationContextNameNotSupported,
323    CallingAETitleNotRecognized,
324    CalledAETitleNotRecognized,
325    Reserved(u8),
326}
327
328impl Display for AssociationRJServiceUserReason {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        match self {
331            AssociationRJServiceUserReason::NoReasonGiven => f.write_str("no reason given"),
332            AssociationRJServiceUserReason::ApplicationContextNameNotSupported => {
333                f.write_str("application context name not supported")
334            }
335            AssociationRJServiceUserReason::CallingAETitleNotRecognized => {
336                f.write_str("calling AE title not recognized")
337            }
338            AssociationRJServiceUserReason::CalledAETitleNotRecognized => {
339                f.write_str("called AE title not recognized")
340            }
341            AssociationRJServiceUserReason::Reserved(code) => write!(f, "reserved code {code}"),
342        }
343    }
344}
345
346#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
347pub enum AssociationRJServiceProviderASCEReason {
348    NoReasonGiven,
349    ProtocolVersionNotSupported,
350}
351
352impl Display for AssociationRJServiceProviderASCEReason {
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        match self {
355            AssociationRJServiceProviderASCEReason::NoReasonGiven => f.write_str("no reason given"),
356            AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => {
357                f.write_str("protocol version not supported")
358            }
359        }
360    }
361}
362
363#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
364pub enum AssociationRJServiceProviderPresentationReason {
365    TemporaryCongestion,
366    LocalLimitExceeded,
367    Reserved(u8),
368}
369
370impl Display for AssociationRJServiceProviderPresentationReason {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        match self {
373            AssociationRJServiceProviderPresentationReason::TemporaryCongestion => {
374                f.write_str("temporary congestion")
375            }
376            AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => {
377                f.write_str("local limit exceeded")
378            }
379            AssociationRJServiceProviderPresentationReason::Reserved(code) => {
380                write!(f, "reserved code {code}")
381            }
382        }
383    }
384}
385
386#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
387pub struct PDataValue {
388    pub presentation_context_id: u8,
389    pub value_type: PDataValueType,
390    pub is_last: bool,
391    pub data: Vec<u8>,
392}
393
394#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
395pub enum PDataValueType {
396    Command,
397    Data,
398}
399
400#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
401pub enum AbortRQSource {
402    ServiceUser,
403    ServiceProvider(AbortRQServiceProviderReason),
404    Reserved,
405}
406
407impl AbortRQSource {
408    fn from(source: u8, reason: u8) -> Option<AbortRQSource> {
409        let result = match (source, reason) {
410            (0, _) => AbortRQSource::ServiceUser,
411            (1, _) => AbortRQSource::Reserved,
412            (2, 0) => {
413                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::ReasonNotSpecified)
414            }
415            (2, 1) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnrecognizedPdu),
416            (2, 2) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnexpectedPdu),
417            (2, 3) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::Reserved),
418            (2, 4) => AbortRQSource::ServiceProvider(
419                AbortRQServiceProviderReason::UnrecognizedPduParameter,
420            ),
421            (2, 5) => {
422                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnexpectedPduParameter)
423            }
424            (2, 6) => {
425                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::InvalidPduParameter)
426            }
427            (_, _) => {
428                return None;
429            }
430        };
431
432        Some(result)
433    }
434}
435
436/// An enumeration of supported A-ABORT PDU provider reasons.
437#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
438pub enum AbortRQServiceProviderReason {
439    /// Reason Not Specified
440    ReasonNotSpecified,
441    /// Unrecognized PDU
442    UnrecognizedPdu,
443    /// Unexpected PDU
444    UnexpectedPdu,
445    /// Reserved
446    Reserved,
447    /// Unrecognized PDU parameter
448    UnrecognizedPduParameter,
449    /// Unexpected PDU parameter
450    UnexpectedPduParameter,
451    /// Invalid PDU parameter
452    InvalidPduParameter,
453}
454
455impl Display for AbortRQServiceProviderReason {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        let msg = match self {
458            AbortRQServiceProviderReason::ReasonNotSpecified => "reason not specified",
459            AbortRQServiceProviderReason::UnrecognizedPdu => "unrecognized PDU",
460            AbortRQServiceProviderReason::UnexpectedPdu => "unexpected PDU",
461            AbortRQServiceProviderReason::Reserved => "reserved code",
462            AbortRQServiceProviderReason::UnrecognizedPduParameter => "unrecognized PDU parameter",
463            AbortRQServiceProviderReason::UnexpectedPduParameter => "unexpected PDU parameter",
464            AbortRQServiceProviderReason::InvalidPduParameter => "invalid PDU parameter",
465        };
466        f.write_str(msg)
467    }
468}
469
470#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
471pub enum PduVariableItem {
472    Unknown(u8),
473    ApplicationContext(String),
474    PresentationContextProposed(PresentationContextProposed),
475    PresentationContextResult(PresentationContextResult),
476    UserVariables(Vec<UserVariableItem>),
477}
478
479#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
480pub enum UserVariableItem {
481    Unknown(u8, Vec<u8>),
482    MaxLength(u32),
483    ImplementationClassUID(String),
484    ImplementationVersionName(String),
485    SopClassExtendedNegotiationSubItem(String, Vec<u8>),
486    UserIdentityItem(UserIdentity),
487}
488
489#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
490pub struct UserIdentity {
491    positive_response_requested: bool,
492    identity_type: UserIdentityType,
493    primary_field: Vec<u8>,
494    secondary_field: Vec<u8>,
495}
496impl UserIdentity {
497    pub fn new(
498        positive_response_requested: bool,
499        identity_type: UserIdentityType,
500        primary_field: Vec<u8>,
501        secondary_field: Vec<u8>,
502    ) -> Self {
503        UserIdentity {
504            positive_response_requested,
505            identity_type,
506            primary_field,
507            secondary_field,
508        }
509    }
510
511    pub fn positive_response_requested(&self) -> bool {
512        self.positive_response_requested
513    }
514
515    pub fn identity_type(&self) -> UserIdentityType {
516        self.identity_type.clone()
517    }
518
519    pub fn primary_field(&self) -> Vec<u8> {
520        self.primary_field.clone()
521    }
522
523    pub fn secondary_field(&self) -> Vec<u8> {
524        self.secondary_field.clone()
525    }
526}
527
528#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
529#[non_exhaustive]
530pub enum UserIdentityType {
531    Username,
532    UsernamePassword,
533    KerberosServiceTicket,
534    SamlAssertion,
535    Jwt,
536}
537impl UserIdentityType {
538    fn from(user_identity_type: u8) -> Option<Self> {
539        match user_identity_type {
540            1 => Some(Self::Username),
541            2 => Some(Self::UsernamePassword),
542            3 => Some(Self::KerberosServiceTicket),
543            4 => Some(Self::SamlAssertion),
544            5 => Some(Self::Jwt),
545            _ => None,
546        }
547    }
548
549    fn to_u8(&self) -> u8 {
550        match self {
551            Self::Username => 1,
552            Self::UsernamePassword => 2,
553            Self::KerberosServiceTicket => 3,
554            Self::SamlAssertion => 4,
555            Self::Jwt => 5,
556        }
557    }
558}
559
560/// An in-memory representation of a full Protocol Data Unit (PDU).
561#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash)]
562pub enum Pdu {
563    /// Unrecognized PDU type
564    Unknown { pdu_type: u8, data: Vec<u8> },
565    /// Association request (A-ASSOCIATION-RQ)
566    AssociationRQ(AssociationRQ),
567    /// Association acknowledgement (A-ASSOCIATION-AC)
568    AssociationAC(AssociationAC),
569    /// Association rejection (A-ASSOCIATION-RJ)
570    AssociationRJ(AssociationRJ),
571    /// P-Data
572    PData { data: Vec<PDataValue> },
573    /// Association release request (A-RELEASE-RQ)
574    ReleaseRQ,
575    /// Association release reply (A-RELEASE-RP)
576    ReleaseRP,
577    /// Association abort request (A-ABORT-RQ)
578    AbortRQ { source: AbortRQSource },
579}
580
581impl Pdu {
582    /// Provide a short description of the PDU.
583    pub fn short_description(&self) -> impl std::fmt::Display + '_ {
584        PduShortDescription(self)
585    }
586}
587
588struct PduShortDescription<'a>(&'a Pdu);
589
590impl std::fmt::Display for PduShortDescription<'_> {
591    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592        match self.0 {
593            Pdu::Unknown { pdu_type, data } => {
594                write!(
595                    f,
596                    "Unknown {{pdu_type: {}, data: {} bytes }}",
597                    pdu_type,
598                    data.len()
599                )
600            }
601            Pdu::AssociationRQ { .. }
602            | Pdu::AssociationAC { .. }
603            | Pdu::AssociationRJ { .. }
604            | Pdu::ReleaseRQ
605            | Pdu::ReleaseRP
606            | Pdu::AbortRQ { .. } => std::fmt::Debug::fmt(self.0, f),
607            Pdu::PData { data } => {
608                if data.len() == 1 {
609                    write!(
610                        f,
611                        "PData [({:?}, {} bytes)]",
612                        data[0].value_type,
613                        data[0].data.len()
614                    )
615                } else if data.len() == 2 {
616                    write!(
617                        f,
618                        "PData [({:?}, {} bytes), ({:?}, {} bytes)]",
619                        data[0].value_type,
620                        data[0].data.len(),
621                        data[1].value_type,
622                        data[1].data.len(),
623                    )
624                } else {
625                    write!(f, "PData [{} p-data values]", data.len())
626                }
627            }
628        }
629    }
630}
631
632/// An in-memory representation of an association request
633#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
634pub struct AssociationRQ {
635    pub protocol_version: u16,
636    pub calling_ae_title: String,
637    pub called_ae_title: String,
638    pub application_context_name: String,
639    pub presentation_contexts: Vec<PresentationContextProposed>,
640    pub user_variables: Vec<UserVariableItem>,
641}
642
643impl From<AssociationRQ> for Pdu {
644    fn from(value: AssociationRQ) -> Self {
645        Pdu::AssociationRQ(value)
646    }
647}
648
649/// An in-memory representation of an association acknowledgement
650#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
651pub struct AssociationAC {
652    pub protocol_version: u16,
653    pub calling_ae_title: String,
654    pub called_ae_title: String,
655    pub application_context_name: String,
656    pub presentation_contexts: Vec<PresentationContextResult>,
657    pub user_variables: Vec<UserVariableItem>,
658}
659
660impl From<AssociationAC> for Pdu {
661    fn from(value: AssociationAC) -> Self {
662        Pdu::AssociationAC(value)
663    }
664}
665
666/// An in-memory representation of an association rejection.
667#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
668pub struct AssociationRJ {
669    pub result: AssociationRJResult,
670    pub source: AssociationRJSource,
671}
672
673impl From<AssociationRJ> for Pdu {
674    fn from(value: AssociationRJ) -> Self {
675        Pdu::AssociationRJ(value)
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use crate::pdu::{PDataValue, PDataValueType};
682
683    use super::Pdu;
684
685    #[test]
686    fn pdu_short_description() {
687        let pdu = Pdu::AbortRQ {
688            source: super::AbortRQSource::ServiceUser,
689        };
690        assert_eq!(
691            &pdu.short_description().to_string(),
692            "AbortRQ { source: ServiceUser }",
693        );
694
695        let pdu = Pdu::PData {
696            data: vec![PDataValue {
697                is_last: true,
698                presentation_context_id: 2,
699                value_type: PDataValueType::Data,
700                data: vec![0x55; 384],
701            }],
702        };
703        assert_eq!(
704            &pdu.short_description().to_string(),
705            "PData [(Data, 384 bytes)]",
706        );
707    }
708}