1pub 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
16pub const PDU_HEADER_SIZE: u32 = 6;
21
22pub const PDV_HEADER_SIZE: u32 = 6;
27
28pub const DEFAULT_MAX_PDU: u32 = 16_384 - PDU_HEADER_SIZE;
30
31pub const MINIMUM_PDU_SIZE: u32 = 1_024 - PDU_HEADER_SIZE;
34
35pub(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 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#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
159pub struct PresentationContextProposed {
160 pub id: u8,
162 pub abstract_syntax: String,
165 pub transfer_syntaxes: Vec<String>,
167}
168
169#[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#[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, 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#[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)]
438pub enum AbortRQServiceProviderReason {
439 ReasonNotSpecified,
441 UnrecognizedPdu,
443 UnexpectedPdu,
445 Reserved,
447 UnrecognizedPduParameter,
449 UnexpectedPduParameter,
451 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#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash)]
562pub enum Pdu {
563 Unknown { pdu_type: u8, data: Vec<u8> },
565 AssociationRQ(AssociationRQ),
567 AssociationAC(AssociationAC),
569 AssociationRJ(AssociationRJ),
571 PData { data: Vec<PDataValue> },
573 ReleaseRQ,
575 ReleaseRP,
577 AbortRQ { source: AbortRQSource },
579}
580
581impl Pdu {
582 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#[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#[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#[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}