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;
13pub use writer::write_pdu;
14
15/// Message component for a proposed presentation context.
16#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
17pub struct PresentationContextProposed {
18    /// the presentation context identifier
19    pub id: u8,
20    /// the expected abstract syntax UID
21    /// (commonly referrering to the expected SOP class)
22    pub abstract_syntax: String,
23    /// a list of transfer syntax UIDs to support in this interaction
24    pub transfer_syntaxes: Vec<String>,
25}
26
27#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
28pub struct PresentationContextResult {
29    pub id: u8,
30    pub reason: PresentationContextResultReason,
31    pub transfer_syntax: String,
32}
33
34#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
35pub enum PresentationContextResultReason {
36    Acceptance = 0,
37    UserRejection = 1,
38    NoReason = 2,
39    AbstractSyntaxNotSupported = 3,
40    TransferSyntaxesNotSupported = 4,
41}
42
43impl PresentationContextResultReason {
44    fn from(reason: u8) -> Option<PresentationContextResultReason> {
45        let result = match reason {
46            0 => PresentationContextResultReason::Acceptance,
47            1 => PresentationContextResultReason::UserRejection,
48            2 => PresentationContextResultReason::NoReason,
49            3 => PresentationContextResultReason::AbstractSyntaxNotSupported,
50            4 => PresentationContextResultReason::TransferSyntaxesNotSupported,
51            _ => {
52                return None;
53            }
54        };
55
56        Some(result)
57    }
58}
59
60impl Display for PresentationContextResultReason {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        let msg = match self {
63            PresentationContextResultReason::Acceptance => "acceptance",
64            PresentationContextResultReason::UserRejection => "user rejection",
65            PresentationContextResultReason::NoReason => "no reason",
66            PresentationContextResultReason::AbstractSyntaxNotSupported => {
67                "abstract syntax not supported"
68            }
69            PresentationContextResultReason::TransferSyntaxesNotSupported => {
70                "transfer syntaxes not supported"
71            }
72        };
73        f.write_str(msg)
74    }
75}
76
77#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
78pub enum AssociationRJResult {
79    Permanent = 1,
80    Transient = 2,
81}
82
83impl AssociationRJResult {
84    fn from(value: u8) -> Option<AssociationRJResult> {
85        match value {
86            1 => Some(AssociationRJResult::Permanent),
87            2 => Some(AssociationRJResult::Transient),
88            _ => None,
89        }
90    }
91}
92
93#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
94pub enum AssociationRJSource {
95    ServiceUser(AssociationRJServiceUserReason),
96    ServiceProviderASCE(AssociationRJServiceProviderASCEReason),
97    ServiceProviderPresentation(AssociationRJServiceProviderPresentationReason),
98}
99
100impl AssociationRJSource {
101    fn from(source: u8, reason: u8) -> Option<AssociationRJSource> {
102        let result = match (source, reason) {
103            (1, 1) => {
104                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::NoReasonGiven)
105            }
106            (1, 2) => AssociationRJSource::ServiceUser(
107                AssociationRJServiceUserReason::ApplicationContextNameNotSupported,
108            ),
109            (1, 3) => AssociationRJSource::ServiceUser(
110                AssociationRJServiceUserReason::CallingAETitleNotRecognized,
111            ),
112            (1, x) if x == 4 || x == 5 || x == 6 => {
113                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::Reserved(x))
114            }
115            (1, 7) => AssociationRJSource::ServiceUser(
116                AssociationRJServiceUserReason::CalledAETitleNotRecognized,
117            ),
118            //(1, 8) | (1, 9) | (1, 10) => {
119            (1, x) if x == 8 || x == 9 || x == 10 => {
120                AssociationRJSource::ServiceUser(AssociationRJServiceUserReason::Reserved(x))
121            }
122            (1, _) => {
123                return None;
124            }
125            (2, 1) => AssociationRJSource::ServiceProviderASCE(
126                AssociationRJServiceProviderASCEReason::NoReasonGiven,
127            ),
128            (2, 2) => AssociationRJSource::ServiceProviderASCE(
129                AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported,
130            ),
131            (2, _) => {
132                return None;
133            }
134            (3, 0) => AssociationRJSource::ServiceProviderPresentation(
135                AssociationRJServiceProviderPresentationReason::Reserved(0),
136            ),
137            (3, 1) => AssociationRJSource::ServiceProviderPresentation(
138                AssociationRJServiceProviderPresentationReason::TemporaryCongestion,
139            ),
140            (3, 2) => AssociationRJSource::ServiceProviderPresentation(
141                AssociationRJServiceProviderPresentationReason::LocalLimitExceeded,
142            ),
143            (3, x) if x == 3 || x == 4 || x == 5 || x == 6 || x == 7 => {
144                AssociationRJSource::ServiceProviderPresentation(
145                    AssociationRJServiceProviderPresentationReason::Reserved(x),
146                )
147            }
148            _ => {
149                return None;
150            }
151        };
152        Some(result)
153    }
154}
155
156impl Display for AssociationRJSource {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            AssociationRJSource::ServiceUser(r) => Display::fmt(r, f),
160            AssociationRJSource::ServiceProviderASCE(r) => Display::fmt(r, f),
161            AssociationRJSource::ServiceProviderPresentation(r) => Display::fmt(r, f),
162        }
163    }
164}
165
166#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
167pub enum AssociationRJServiceUserReason {
168    NoReasonGiven,
169    ApplicationContextNameNotSupported,
170    CallingAETitleNotRecognized,
171    CalledAETitleNotRecognized,
172    Reserved(u8),
173}
174
175impl Display for AssociationRJServiceUserReason {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            AssociationRJServiceUserReason::NoReasonGiven => f.write_str("no reason given"),
179            AssociationRJServiceUserReason::ApplicationContextNameNotSupported => {
180                f.write_str("application context name not supported")
181            }
182            AssociationRJServiceUserReason::CallingAETitleNotRecognized => {
183                f.write_str("calling AE title not recognized")
184            }
185            AssociationRJServiceUserReason::CalledAETitleNotRecognized => {
186                f.write_str("called AE title not recognized")
187            }
188            AssociationRJServiceUserReason::Reserved(code) => write!(f, "reserved code {}", code),
189        }
190    }
191}
192
193#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
194pub enum AssociationRJServiceProviderASCEReason {
195    NoReasonGiven,
196    ProtocolVersionNotSupported,
197}
198
199impl Display for AssociationRJServiceProviderASCEReason {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        match self {
202            AssociationRJServiceProviderASCEReason::NoReasonGiven => f.write_str("no reason given"),
203            AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => {
204                f.write_str("protocol version not supported")
205            }
206        }
207    }
208}
209
210#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
211pub enum AssociationRJServiceProviderPresentationReason {
212    TemporaryCongestion,
213    LocalLimitExceeded,
214    Reserved(u8),
215}
216
217impl Display for AssociationRJServiceProviderPresentationReason {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        match self {
220            AssociationRJServiceProviderPresentationReason::TemporaryCongestion => {
221                f.write_str("temporary congestion")
222            }
223            AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => {
224                f.write_str("local limit exceeded")
225            }
226            AssociationRJServiceProviderPresentationReason::Reserved(code) => {
227                write!(f, "reserved code {}", code)
228            }
229        }
230    }
231}
232
233#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
234pub struct PDataValue {
235    pub presentation_context_id: u8,
236    pub value_type: PDataValueType,
237    pub is_last: bool,
238    pub data: Vec<u8>,
239}
240
241#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
242pub enum PDataValueType {
243    Command,
244    Data,
245}
246
247#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
248pub enum AbortRQSource {
249    ServiceUser,
250    ServiceProvider(AbortRQServiceProviderReason),
251    Reserved,
252}
253
254impl AbortRQSource {
255    fn from(source: u8, reason: u8) -> Option<AbortRQSource> {
256        let result = match (source, reason) {
257            (0, _) => AbortRQSource::ServiceUser,
258            (1, _) => AbortRQSource::Reserved,
259            (2, 0) => {
260                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::ReasonNotSpecified)
261            }
262            (2, 1) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnrecognizedPdu),
263            (2, 2) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnexpectedPdu),
264            (2, 3) => AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::Reserved),
265            (2, 4) => AbortRQSource::ServiceProvider(
266                AbortRQServiceProviderReason::UnrecognizedPduParameter,
267            ),
268            (2, 5) => {
269                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::UnexpectedPduParameter)
270            }
271            (2, 6) => {
272                AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::InvalidPduParameter)
273            }
274            (_, _) => {
275                return None;
276            }
277        };
278
279        Some(result)
280    }
281}
282
283/// An enumeration of supported A-ABORT PDU provider reasons.
284#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
285pub enum AbortRQServiceProviderReason {
286    /// Reason Not Specified
287    ReasonNotSpecified,
288    /// Unrecognized PDU
289    UnrecognizedPdu,
290    /// Unexpected PDU
291    UnexpectedPdu,
292    /// Reserved
293    Reserved,
294    /// Unrecognized PDU parameter
295    UnrecognizedPduParameter,
296    /// Unexpected PDU parameter
297    UnexpectedPduParameter,
298    /// Invalid PDU parameter
299    InvalidPduParameter,
300}
301
302impl Display for AbortRQServiceProviderReason {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        let msg = match self {
305            AbortRQServiceProviderReason::ReasonNotSpecified => "reason not specified",
306            AbortRQServiceProviderReason::UnrecognizedPdu => "unrecognized PDU",
307            AbortRQServiceProviderReason::UnexpectedPdu => "unexpected PDU",
308            AbortRQServiceProviderReason::Reserved => "reserved code",
309            AbortRQServiceProviderReason::UnrecognizedPduParameter => "unrecognized PDU parameter",
310            AbortRQServiceProviderReason::UnexpectedPduParameter => "unexpected PDU parameter",
311            AbortRQServiceProviderReason::InvalidPduParameter => "invalid PDU parameter",
312        };
313        f.write_str(msg)
314    }
315}
316
317#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
318pub enum PduVariableItem {
319    Unknown(u8),
320    ApplicationContext(String),
321    PresentationContextProposed(PresentationContextProposed),
322    PresentationContextResult(PresentationContextResult),
323    UserVariables(Vec<UserVariableItem>),
324}
325
326#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
327pub enum UserVariableItem {
328    Unknown(u8, Vec<u8>),
329    MaxLength(u32),
330    ImplementationClassUID(String),
331    ImplementationVersionName(String),
332    SopClassExtendedNegotiationSubItem(String, Vec<u8>),
333    UserIdentityItem(UserIdentity),
334}
335
336#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
337pub struct UserIdentity {
338    positive_response_requested: bool,
339    identity_type: UserIdentityType,
340    primary_field: Vec<u8>,
341    secondary_field: Vec<u8>,
342}
343impl UserIdentity {
344    pub fn new(
345        positive_response_requested: bool,
346        identity_type: UserIdentityType,
347        primary_field: Vec<u8>,
348        secondary_field: Vec<u8>,
349    ) -> Self {
350        UserIdentity {
351            positive_response_requested,
352            identity_type,
353            primary_field,
354            secondary_field,
355        }
356    }
357
358    pub fn positive_response_requested(&self) -> bool {
359        self.positive_response_requested
360    }
361
362    pub fn identity_type(&self) -> UserIdentityType {
363        self.identity_type.clone()
364    }
365
366    pub fn primary_field(&self) -> Vec<u8> {
367        self.primary_field.clone()
368    }
369
370    pub fn secondary_field(&self) -> Vec<u8> {
371        self.secondary_field.clone()
372    }
373}
374
375#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
376#[non_exhaustive]
377pub enum UserIdentityType {
378    Username,
379    UsernamePassword,
380    KerberosServiceTicket,
381    SamlAssertion,
382    Jwt,
383}
384impl UserIdentityType {
385    fn from(user_identity_type: u8) -> Option<Self> {
386        match user_identity_type {
387            1 => Some(Self::Username),
388            2 => Some(Self::UsernamePassword),
389            3 => Some(Self::KerberosServiceTicket),
390            4 => Some(Self::SamlAssertion),
391            5 => Some(Self::Jwt),
392            _ => None,
393        }
394    }
395
396    fn to_u8(&self) -> u8 {
397        match self {
398            Self::Username => 1,
399            Self::UsernamePassword => 2,
400            Self::KerberosServiceTicket => 3,
401            Self::SamlAssertion => 4,
402            Self::Jwt => 5,
403        }
404    }
405}
406
407/// An in-memory representation of a full Protocol Data Unit (PDU).
408#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash)]
409pub enum Pdu {
410    /// Unrecognized PDU type
411    Unknown { pdu_type: u8, data: Vec<u8> },
412    /// Association request (A-ASSOCIATION-RQ)
413    AssociationRQ(AssociationRQ),
414    /// Association acknowledgement (A-ASSOCIATION-AC)
415    AssociationAC(AssociationAC),
416    /// Association rejection (A-ASSOCIATION-RJ)
417    AssociationRJ(AssociationRJ),
418    /// P-Data
419    PData { data: Vec<PDataValue> },
420    /// Association release request (A-RELEASE-RQ)
421    ReleaseRQ,
422    /// Association release reply (A-RELEASE-RP)
423    ReleaseRP,
424    /// Association abort request (A-ABORT-RQ)
425    AbortRQ { source: AbortRQSource },
426}
427
428impl Pdu {
429    /// Provide a short description of the PDU.
430    pub fn short_description(&self) -> impl std::fmt::Display + '_ {
431        PduShortDescription(self)
432    }
433}
434
435struct PduShortDescription<'a>(&'a Pdu);
436
437impl std::fmt::Display for PduShortDescription<'_> {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        match self.0 {
440            Pdu::Unknown { pdu_type, data } => {
441                write!(
442                    f,
443                    "Unknown {{pdu_type: {}, data: {} bytes }}",
444                    pdu_type,
445                    data.len()
446                )
447            }
448            Pdu::AssociationRQ { .. }
449            | Pdu::AssociationAC { .. }
450            | Pdu::AssociationRJ { .. }
451            | Pdu::ReleaseRQ
452            | Pdu::ReleaseRP
453            | Pdu::AbortRQ { .. } => std::fmt::Debug::fmt(self.0, f),
454            Pdu::PData { data } => {
455                if data.len() == 1 {
456                    write!(
457                        f,
458                        "PData [({:?}, {} bytes)]",
459                        data[0].value_type,
460                        data[0].data.len()
461                    )
462                } else if data.len() == 2 {
463                    write!(
464                        f,
465                        "PData [({:?}, {} bytes), ({:?}, {} bytes)]",
466                        data[0].value_type,
467                        data[0].data.len(),
468                        data[1].value_type,
469                        data[1].data.len(),
470                    )
471                } else {
472                    write!(f, "PData [{} p-data values]", data.len())
473                }
474            }
475        }
476    }
477}
478
479/// An in-memory representation of an association request
480#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
481pub struct AssociationRQ {
482    pub protocol_version: u16,
483    pub calling_ae_title: String,
484    pub called_ae_title: String,
485    pub application_context_name: String,
486    pub presentation_contexts: Vec<PresentationContextProposed>,
487    pub user_variables: Vec<UserVariableItem>,
488}
489
490impl From<AssociationRQ> for Pdu {
491    fn from(value: AssociationRQ) -> Self {
492        Pdu::AssociationRQ(value)
493    }
494}
495
496/// An in-memory representation of an association acknowledgement
497#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
498pub struct AssociationAC {
499    pub protocol_version: u16,
500    pub calling_ae_title: String,
501    pub called_ae_title: String,
502    pub application_context_name: String,
503    pub presentation_contexts: Vec<PresentationContextResult>,
504    pub user_variables: Vec<UserVariableItem>,
505}
506
507impl From<AssociationAC> for Pdu {
508    fn from(value: AssociationAC) -> Self {
509        Pdu::AssociationAC(value)
510    }
511}
512
513/// An in-memory representation of an association rejection.
514#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd)]
515pub struct AssociationRJ {
516    pub result: AssociationRJResult,
517    pub source: AssociationRJSource,
518}
519
520impl From<AssociationRJ> for Pdu {
521    fn from(value: AssociationRJ) -> Self {
522        Pdu::AssociationRJ(value)
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use crate::pdu::{PDataValue, PDataValueType};
529
530    use super::Pdu;
531
532    #[test]
533    fn pdu_short_description() {
534        let pdu = Pdu::AbortRQ {
535            source: super::AbortRQSource::ServiceUser,
536        };
537        assert_eq!(
538            &pdu.short_description().to_string(),
539            "AbortRQ { source: ServiceUser }",
540        );
541
542        let pdu = Pdu::PData {
543            data: vec![PDataValue {
544                is_last: true,
545                presentation_context_id: 2,
546                value_type: PDataValueType::Data,
547                data: vec![0x55; 384],
548            }],
549        };
550        assert_eq!(
551            &pdu.short_description().to_string(),
552            "PData [(Data, 384 bytes)]",
553        );
554    }
555}