dicom_ul/pdu/writer.rs
1/// PDU writer module
2use crate::pdu::*;
3use byteordered::byteorder::{BigEndian, WriteBytesExt};
4use dicom_encoding::text::TextCodec;
5use snafu::{Backtrace, ResultExt, Snafu};
6use std::io::Write;
7
8pub type Error = crate::pdu::WriteError;
9
10pub type Result<T> = std::result::Result<T, WriteError>;
11
12#[derive(Debug, Snafu)]
13pub enum WriteChunkError {
14 #[snafu(display("Failed to build chunk"))]
15 BuildChunk {
16 #[snafu(backtrace)]
17 source: Box<WriteError>,
18 },
19 #[snafu(display("Failed to write chunk length"))]
20 WriteLength {
21 backtrace: Backtrace,
22 source: std::io::Error,
23 },
24 #[snafu(display("Failed to write chunk data"))]
25 WriteData {
26 backtrace: Backtrace,
27 source: std::io::Error,
28 },
29}
30
31fn write_chunk_u32<F>(writer: &mut dyn Write, func: F) -> std::result::Result<(), WriteChunkError>
32where
33 F: FnOnce(&mut Vec<u8>) -> Result<()>,
34{
35 let mut data = vec![];
36 func(&mut data)
37 .map_err(Box::from)
38 .context(BuildChunkSnafu)?;
39
40 let length = data.len() as u32;
41 writer
42 .write_u32::<BigEndian>(length)
43 .context(WriteLengthSnafu)?;
44
45 writer.write_all(&data).context(WriteDataSnafu)?;
46
47 Ok(())
48}
49
50fn write_chunk_u16<F>(writer: &mut dyn Write, func: F) -> std::result::Result<(), WriteChunkError>
51where
52 F: FnOnce(&mut Vec<u8>) -> Result<()>,
53{
54 let mut data = vec![];
55 func(&mut data)
56 .map_err(Box::from)
57 .context(BuildChunkSnafu)?;
58
59 let length = data.len() as u16;
60 writer
61 .write_u16::<BigEndian>(length)
62 .context(WriteLengthSnafu)?;
63
64 writer.write_all(&data).context(WriteDataSnafu)?;
65
66 Ok(())
67}
68
69pub fn write_pdu<W>(writer: &mut W, pdu: &Pdu) -> Result<()>
70where
71 W: Write,
72{
73 let codec = dicom_encoding::text::DefaultCharacterSetCodec;
74 match pdu {
75 Pdu::AssociationRQ(AssociationRQ {
76 protocol_version,
77 calling_ae_title,
78 called_ae_title,
79 application_context_name,
80 presentation_contexts,
81 user_variables,
82 }) => {
83 // A-ASSOCIATE-RQ PDU Structure
84
85 // 1 - PDU-type - 01H
86 writer
87 .write_u8(0x01)
88 .context(WriteFieldSnafu { field: "PDU-type" })?;
89
90 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
91 // tested to this value when received.
92 writer
93 .write_u8(0x00)
94 .context(WriteReservedSnafu { bytes: 1_u32 })?;
95
96 write_chunk_u32(writer, |writer| {
97 // 7-8 Protocol-version - This two byte field shall use one bit to identify
98 // each version of the DICOM UL protocol supported by the calling end-system.
99 // This is Version 1 and shall be identified with bit 0 set. A receiver of this
100 // PDU implementing only this version of the DICOM UL protocol shall only test
101 // that bit 0 is set.
102 writer
103 .write_u16::<BigEndian>(*protocol_version)
104 .context(WriteFieldSnafu {
105 field: "Protocol-version",
106 })?;
107
108 // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but
109 // not tested to this value when received.
110 writer
111 .write_u16::<BigEndian>(0x00)
112 .context(WriteReservedSnafu { bytes: 2_u32 })?;
113
114 // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be
115 // encoded as 16 characters as defined by the ISO 646:1990-Basic G0 Set with
116 // leading and trailing spaces (20H) being non-significant. The value made of 16
117 // spaces (20H) meaning "no Application Name specified" shall not be used. For a
118 // complete description of the use of this field, see Section 7.1.1.4.
119 let mut ae_title_bytes =
120 codec.encode(called_ae_title).context(EncodeFieldSnafu {
121 field: "Called-AE-title",
122 })?;
123 ae_title_bytes.resize(16, b' ');
124 writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
125 field: "Called-AE-title",
126 })?;
127
128 // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded
129 // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and
130 // trailing spaces (20H) being non-significant. The value made of 16 spaces
131 // (20H) meaning "no Application Name specified" shall not be used. For a
132 // complete description of the use of this field, see Section 7.1.1.3.
133 let mut ae_title_bytes =
134 codec.encode(calling_ae_title).context(EncodeFieldSnafu {
135 field: "Calling-AE-title",
136 })?;
137 ae_title_bytes.resize(16, b' ');
138 writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
139 field: "Called-AE-title",
140 })?;
141
142 // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all
143 // bytes but not tested to this value when received
144 writer
145 .write_all(&[0; 32])
146 .context(WriteReservedSnafu { bytes: 32_u32 })?;
147
148 write_pdu_variable_application_context_name(
149 writer,
150 application_context_name,
151 &codec,
152 )?;
153
154 for presentation_context in presentation_contexts {
155 write_pdu_variable_presentation_context_proposed(
156 writer,
157 presentation_context,
158 &codec,
159 )?;
160 }
161
162 write_pdu_variable_user_variables(writer, user_variables, &codec)?;
163
164 Ok(())
165 })
166 .context(WriteChunkSnafu {
167 name: "A-ASSOCIATE-RQ",
168 })?;
169
170 Ok(())
171 }
172 Pdu::AssociationAC(AssociationAC {
173 protocol_version,
174 application_context_name,
175 called_ae_title,
176 calling_ae_title,
177 presentation_contexts,
178 user_variables,
179 }) => {
180 // A-ASSOCIATE-AC PDU Structure
181
182 // 1 - PDU-type - 02H
183 writer
184 .write_u8(0x02)
185 .context(WriteFieldSnafu { field: "PDU-type" })?;
186
187 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
188 // this value when received.
189 writer
190 .write_u8(0x00)
191 .context(WriteReservedSnafu { bytes: 1_u32 })?;
192
193 write_chunk_u32(writer, |writer| {
194 // 7-8 - Protocol-version - This two byte field shall use one bit to identify each
195 // version of the DICOM UL protocol supported by the calling end-system. This is
196 // Version 1 and shall be identified with bit 0 set. A receiver of this PDU
197 // implementing only this version of the DICOM UL protocol shall only test that bit
198 // 0 is set.
199 writer
200 .write_u16::<BigEndian>(*protocol_version)
201 .context(WriteFieldSnafu {
202 field: "Protocol-version",
203 })?;
204
205 // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not
206 // tested to this value when received.
207 writer
208 .write_u16::<BigEndian>(0x00)
209 .context(WriteReservedSnafu { bytes: 2_u32 })?;
210
211 // 11-26 - Reserved - This reserved field shall be sent with a value identical to
212 // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
213 // shall not be tested when received.
214 let mut ae_title_bytes =
215 codec.encode(called_ae_title).context(EncodeFieldSnafu {
216 field: "Called-AE-title",
217 })?;
218 ae_title_bytes.resize(16, b' ');
219 writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
220 field: "Called-AE-title",
221 })?;
222 // 27-42 - Reserved - This reserved field shall be sent with a value identical to
223 // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
224 // shall not be tested when received.
225 let mut ae_title_bytes =
226 codec.encode(calling_ae_title).context(EncodeFieldSnafu {
227 field: "Calling-AE-title",
228 })?;
229 ae_title_bytes.resize(16, b' ');
230 writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
231 field: "Calling-AE-title",
232 })?;
233
234 // 43-74 - Reserved - This reserved field shall be sent with a value identical to
235 // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
236 // shall not be tested when received.
237 writer
238 .write_all(&[0; 32])
239 .context(WriteReservedSnafu { bytes: 32_u32 })?;
240
241 // 75-xxx - Variable items - This variable field shall contain the following items:
242 // one Application Context Item, one or more Presentation Context Item(s) and one
243 // User Information Item. For a complete description of these items see Section
244 // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6.
245 write_pdu_variable_application_context_name(
246 writer,
247 application_context_name,
248 &codec,
249 )?;
250
251 for presentation_context in presentation_contexts {
252 write_pdu_variable_presentation_context_result(
253 writer,
254 presentation_context,
255 &codec,
256 )?;
257 }
258
259 write_pdu_variable_user_variables(writer, user_variables, &codec)?;
260
261 Ok(())
262 })
263 .context(WriteChunkSnafu {
264 name: "A-ASSOCIATE-AC",
265 })
266 }
267 Pdu::AssociationRJ(AssociationRJ { result, source }) => {
268 // 1 - PDU-type - 03H
269 writer
270 .write_u8(0x03)
271 .context(WriteFieldSnafu { field: "PDU-type" })?;
272
273 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received.
274 writer
275 .write_u8(0x00)
276 .context(WriteReservedSnafu { bytes: 1_u32 })?;
277
278 write_chunk_u32(writer, |writer| {
279 // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received.
280 writer.write_u8(0x00).context(WriteReservedSnafu { bytes: 1_u32 })?;
281
282 // 8 - Result - This Result field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used:
283 // - 1 - rejected-permanent
284 // - 2 - rejected-transient
285 writer.write_u8(match result {
286 AssociationRJResult::Permanent => {
287 0x01
288 }
289 AssociationRJResult::Transient => {
290 0x02
291 }
292 }).context(WriteFieldSnafu { field: "AssociationRJResult" })?;
293
294 // 9 - Source - This Source field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used:
295 // - 1 - DICOM UL service-user
296 // - 2 - DICOM UL service-provider (ACSE related function)
297 // - 3 - DICOM UL service-provider (Presentation related function)
298 // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned binary number.
299 // If the Source field has the value (1) "DICOM UL service-user", it shall take one of the following:
300 // - 1 - no-reason-given
301 // - 2 - application-context-name-not-supported
302 // - 3 - calling-AE-title-not-recognized
303 // - 4-6 - reserved
304 // - 7 - called-AE-title-not-recognized
305 // - 8-10 - reserved
306 // If the Source field has the value (2) "DICOM UL service provided (ACSE related function)", it shall take one of the following:
307 // - 1 - no-reason-given
308 // - 2 - protocol-version-not-supported
309 // If the Source field has the value (3) "DICOM UL service provided (Presentation related function)", it shall take one of the following:
310 // 0 - reserved
311 // 1 - temporary-congestion
312 // 2 - local-limit-exceeded
313 // 3-7 - reserved
314 match source {
315 AssociationRJSource::ServiceUser(reason) => {
316 writer.write_u8(0x01).context(WriteFieldSnafu { field: "AssociationRJServiceUserReason" })?;
317 writer.write_u8(match reason {
318 AssociationRJServiceUserReason::NoReasonGiven => {
319 0x01
320 }
321 AssociationRJServiceUserReason::ApplicationContextNameNotSupported => {
322 0x02
323 }
324 AssociationRJServiceUserReason::CallingAETitleNotRecognized => {
325 0x03
326 }
327 AssociationRJServiceUserReason::CalledAETitleNotRecognized => {
328 0x07
329 }
330 AssociationRJServiceUserReason::Reserved(data) => {
331 *data
332 }
333 }).context(WriteFieldSnafu { field: "AssociationRJServiceUserReason (2)" })?;
334 }
335 AssociationRJSource::ServiceProviderASCE(reason) => {
336 writer.write_u8(0x02).context(WriteFieldSnafu { field: "AssociationRJServiceProvider" })?;
337 writer.write_u8(match reason {
338 AssociationRJServiceProviderASCEReason::NoReasonGiven => {
339 0x01
340 }
341 AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => {
342 0x02
343 }
344 }).context(WriteFieldSnafu { field: "AssociationRJServiceProvider (2)" })?;
345 }
346 AssociationRJSource::ServiceProviderPresentation(reason) => {
347 writer.write_u8(0x03).context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason" })?;
348 writer.write_u8(match reason {
349 AssociationRJServiceProviderPresentationReason::TemporaryCongestion => {
350 0x01
351 }
352 AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => {
353 0x02
354 }
355 AssociationRJServiceProviderPresentationReason::Reserved(data) => {
356 *data
357 }
358 }).context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason (2)" })?;
359 }
360 }
361
362 Ok(())
363 }).context(WriteChunkSnafu { name: "AssociationRJ" })?;
364
365 Ok(())
366 }
367 Pdu::PData { data } => {
368 // 1 - PDU-type - 04H
369 writer
370 .write_u8(0x04)
371 .context(WriteFieldSnafu { field: "PDU-type" })?;
372
373 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
374 // this value when received.
375 writer
376 .write_u8(0x00)
377 .context(WriteReservedSnafu { bytes: 1_u32 })?;
378
379 write_chunk_u32(writer, |writer| {
380 // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain
381 // one or more Presentation-data-value Items(s). For a complete description of the
382 // use of this field see Section 9.3.5.1
383
384 for presentation_data_value in data {
385 write_chunk_u32(writer, |writer| {
386 // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd
387 // integers between 1 and 255, encoded as an unsigned binary number. For a
388 // complete description of the use of this field see Section 7.1.1.13.
389 writer.push(presentation_data_value.presentation_context_id);
390
391 // 6-xxx - Presentation-data-value - This Presentation-data-value field
392 // shall contain DICOM message information (command and/or data set) with a
393 // message control header. For a complete description of the use of this
394 // field see Annex E.
395
396 // The Message Control Header shall be made of one byte with the least
397 // significant bit (bit 0) taking one of the following values:
398 // - If bit 0 is set to 1, the following fragment shall contain Message
399 // Command information.
400 // - If bit 0 is set to 0, the following fragment shall contain Message Data
401 // Set information.
402 // The next least significant bit (bit 1) shall be defined by the following
403 // rules: If bit 1 is set to 1, the following fragment shall contain the
404 // last fragment of a Message Data Set or of a Message Command.
405 // - If bit 1 is set to 0, the following fragment does not contain the last
406 // fragment of a Message Data Set or of a Message Command.
407 let mut message_header = 0x00;
408 if let PDataValueType::Command = presentation_data_value.value_type {
409 message_header |= 0x01;
410 }
411 if presentation_data_value.is_last {
412 message_header |= 0x02;
413 }
414 writer.push(message_header);
415
416 // Message fragment
417 writer.extend(&presentation_data_value.data);
418
419 Ok(())
420 })
421 .context(WriteChunkSnafu {
422 name: "Presentation-data-value item",
423 })?;
424 }
425
426 Ok(())
427 })
428 .context(WriteChunkSnafu { name: "PData" })
429 }
430 Pdu::ReleaseRQ => {
431 // 1 - PDU-type - 05H
432 writer
433 .write_u8(0x05)
434 .context(WriteFieldSnafu { field: "PDU-type" })?;
435
436 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
437 // this value when received.
438 writer
439 .write_u8(0x00)
440 .context(WriteReservedSnafu { bytes: 1_u32 })?;
441
442 write_chunk_u32(writer, |writer| {
443 writer.extend([0u8; 4]);
444 Ok(())
445 })
446 .context(WriteChunkSnafu { name: "ReleaseRQ" })?;
447
448 Ok(())
449 }
450 Pdu::ReleaseRP => {
451 // 1 - PDU-type - 06H
452 writer
453 .write_u8(0x06)
454 .context(WriteFieldSnafu { field: "PDU-type" })?;
455
456 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
457 // this value when received.
458 writer
459 .write_u8(0x00)
460 .context(WriteReservedSnafu { bytes: 1_u32 })?;
461
462 write_chunk_u32(writer, |writer| {
463 writer.extend([0u8; 4]);
464 Ok(())
465 })
466 .context(WriteChunkSnafu { name: "ReleaseRP" })?;
467
468 Ok(())
469 }
470 Pdu::AbortRQ { source } => {
471 // 1 - PDU-type - 07H
472 writer
473 .write_u8(0x07)
474 .context(WriteFieldSnafu { field: "PDU-type" })?;
475
476 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
477 // this value when received.
478 writer
479 .write_u8(0x00)
480 .context(WriteReservedSnafu { bytes: 1_u32 })?;
481
482 write_chunk_u32(writer, |writer| {
483 // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested
484 // to this value when received.
485 writer.push(0);
486 // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested
487 // to this value when received.
488 writer.push(0);
489
490 // 9 - Source - This Source field shall contain an integer value encoded as an
491 // unsigned binary number. One of the following values shall be used:
492 // - 0 - DICOM UL service-user (initiated abort)
493 // - 1 - reserved
494 // - 2 - DICOM UL service-provider (initiated abort)
495 // 10 - Reason/Diag - This field shall contain an integer value encoded as an
496 // unsigned binary number. If the Source field has the value (2) "DICOM UL
497 // service-provider", it shall take one of the following:
498 // - 0 - reason-not-specified1 - unrecognized-PDU
499 // - 2 - unexpected-PDU
500 // - 3 - reserved
501 // - 4 - unrecognized-PDU parameter
502 // - 5 - unexpected-PDU parameter
503 // - 6 - invalid-PDU-parameter value
504 // If the Source field has the value (0) "DICOM UL service-user", this reason field
505 // shall not be significant. It shall be sent with a value 00H but not tested to
506 // this value when received.
507 let source_word = match source {
508 AbortRQSource::ServiceUser => [0x00; 2],
509 AbortRQSource::Reserved => [0x01, 0x00],
510 AbortRQSource::ServiceProvider(reason) => match reason {
511 AbortRQServiceProviderReason::ReasonNotSpecified => [0x02, 0x00],
512 AbortRQServiceProviderReason::UnrecognizedPdu => [0x02, 0x01],
513 AbortRQServiceProviderReason::UnexpectedPdu => [0x02, 0x02],
514 AbortRQServiceProviderReason::Reserved => [0x02, 0x03],
515 AbortRQServiceProviderReason::UnrecognizedPduParameter => [0x02, 0x04],
516 AbortRQServiceProviderReason::UnexpectedPduParameter => [0x02, 0x05],
517 AbortRQServiceProviderReason::InvalidPduParameter => [0x02, 0x06],
518 },
519 };
520 writer.extend(source_word);
521
522 Ok(())
523 })
524 .context(WriteChunkSnafu { name: "AbortRQ" })?;
525
526 Ok(())
527 }
528 Pdu::Unknown { pdu_type, data } => {
529 // 1 - PDU-type - XXH
530 writer
531 .write_u8(*pdu_type)
532 .context(WriteFieldSnafu { field: "PDU-type" })?;
533
534 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
535 // this value when received.
536 writer
537 .write_u8(0x00)
538 .context(WriteReservedSnafu { bytes: 1_u32 })?;
539
540 write_chunk_u32(writer, |writer| {
541 writer.extend(data);
542 Ok(())
543 })
544 .context(WriteChunkSnafu { name: "Unknown" })?;
545
546 Ok(())
547 }
548 }
549}
550
551fn write_pdu_variable_application_context_name(
552 writer: &mut dyn Write,
553 application_context_name: &str,
554 codec: &dyn TextCodec,
555) -> Result<()> {
556 // Application Context Item Structure
557 // 1 - Item-type - 10H
558 writer
559 .write_u8(0x10)
560 .context(WriteFieldSnafu { field: "Item-type" })?;
561
562 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
563 // tested to this value when received.
564 writer
565 .write_u8(0x00)
566 .context(WriteReservedSnafu { bytes: 1_u32 })?;
567
568 write_chunk_u16(writer, |writer| {
569 // 5-xxx - Application-context-name -A valid Application-context-name shall
570 // be encoded as defined in Annex F. For a description of the use of this
571 // field see Section 7.1.1.2. Application-context-names are structured as
572 // UIDs as defined in PS3.5 (see Annex A for an overview of this concept).
573 // DICOM Application-context-names are registered in PS3.7.
574 writer
575 .write_all(
576 &codec
577 .encode(application_context_name)
578 .context(EncodeFieldSnafu {
579 field: "Application-context-name",
580 })?,
581 )
582 .context(WriteFieldSnafu {
583 field: "Application-context-name",
584 })
585 })
586 .context(WriteChunkSnafu {
587 name: "Application Context Item",
588 })?;
589
590 Ok(())
591}
592
593fn write_pdu_variable_presentation_context_proposed(
594 writer: &mut dyn Write,
595 presentation_context: &PresentationContextProposed,
596 codec: &dyn TextCodec,
597) -> Result<()> {
598 // Presentation Context Item Structure
599 // 1 - tem-type - 20H
600 writer
601 .write_u8(0x20)
602 .context(WriteFieldSnafu { field: "Item-type" })?;
603
604 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
605 // tested to this value when received.
606 writer
607 .write_u8(0x00)
608 .context(WriteReservedSnafu { bytes: 1_u32 })?;
609
610 write_chunk_u16(writer, |writer| {
611 // 5 - Presentation-context-ID - Presentation-context-ID values shall be
612 // odd integers between 1 and 255, encoded as an unsigned binary number.
613 // For a complete description of the use of this field see Section
614 // 7.1.1.13.
615 writer
616 .write_u8(presentation_context.id)
617 .context(WriteFieldSnafu {
618 field: "Presentation-context-ID",
619 })?;
620
621 // 6 - Reserved - This reserved field shall be sent with a value 00H but
622 // not tested to this value when received.
623 writer
624 .write_u8(0x00)
625 .context(WriteReservedSnafu { bytes: 1_u32 })?;
626
627 // 7 - Reserved - This reserved field shall be sent with a value 00H but
628 // not tested to this value when received
629 writer
630 .write_u8(0x00)
631 .context(WriteReservedSnafu { bytes: 1_u32 })?;
632
633 // 8 - Reserved - This reserved field shall be sent with a value 00H but
634 // not tested to this value when received.
635 writer
636 .write_u8(0x00)
637 .context(WriteReservedSnafu { bytes: 1_u32 })?;
638
639 // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field
640 // shall contain the following sub-items: one Abstract Syntax and one or
641 // more Transfer Syntax(es). For a complete description of the use and
642 // encoding of these sub-items see Section 9.3.2.2.1 and Section
643 // 9.3.2.2.2.
644
645 // Abstract Syntax Sub-Item Structure
646 // 1 - Item-type 30H
647 writer
648 .write_u8(0x30)
649 .context(WriteFieldSnafu { field: "Item-type" })?;
650
651 // 2 - Reserved - This reserved field shall be sent with a value 00H
652 // but not tested to this value when
653 // received.
654 writer
655 .write_u8(0x00)
656 .context(WriteReservedSnafu { bytes: 1_u32 })?;
657
658 write_chunk_u16(writer, |writer| {
659 // 5-xxx - Abstract-syntax-name - This variable field shall
660 // contain
661 // the Abstract-syntax-name related to the proposed presentation
662 // context. A valid Abstract-syntax-name shall be encoded as
663 // defined in Annex F. For a
664 // description of the use of this field see
665 // Section 7.1.1.13. Abstract-syntax-names are structured as
666 // UIDs as defined in PS3.5
667 // (see Annex B for an overview of this concept).
668 // DICOM Abstract-syntax-names are registered in PS3.4.
669 writer
670 .write_all(
671 &codec
672 .encode(&presentation_context.abstract_syntax)
673 .context(EncodeFieldSnafu {
674 field: "Abstract-syntax-name",
675 })?,
676 )
677 .context(WriteFieldSnafu {
678 field: "Abstract-syntax-name",
679 })
680 })
681 .context(WriteChunkSnafu {
682 name: "Abstract Syntax Item",
683 })?;
684
685 for transfer_syntax in &presentation_context.transfer_syntaxes {
686 // Transfer Syntax Sub-Item Structure
687 // 1 - Item-type - 40H
688 writer.write_u8(0x40).context(WriteFieldSnafu {
689 field: "Presentation-context Item-type",
690 })?;
691
692 // 2 - Reserved - This reserved field shall be sent with a value 00H
693 // but not tested to this value when received.
694 writer
695 .write_u8(0x00)
696 .context(WriteReservedSnafu { bytes: 1_u32 })?;
697
698 write_chunk_u16(writer, |writer| {
699 // 5-xxx - Transfer-syntax-name(s) - This variable field shall
700 // contain the Transfer-syntax-name proposed for this
701 // presentation context. A valid Transfer-syntax-name shall be
702 // encoded as defined in Annex F. For a description of the use
703 // of this field see Section 7.1.1.13. Transfer-syntax-names are
704 // structured as UIDs as defined in PS3.5 (see Annex B for an
705 // overview of this concept). DICOM Transfer-syntax-names are
706 // registered in PS3.5.
707 writer
708 .write_all(&codec.encode(transfer_syntax).context(EncodeFieldSnafu {
709 field: "Transfer-syntax-name",
710 })?)
711 .context(WriteFieldSnafu {
712 field: "Transfer-syntax-name",
713 })
714 })
715 .context(WriteChunkSnafu {
716 name: "Transfer Syntax Sub-Item",
717 })?;
718 }
719
720 Ok(())
721 })
722 .context(WriteChunkSnafu {
723 name: "Presentation Context Item",
724 })?;
725
726 Ok(())
727}
728
729fn write_pdu_variable_presentation_context_result(
730 writer: &mut dyn Write,
731 presentation_context: &PresentationContextResult,
732 codec: &dyn TextCodec,
733) -> Result<()> {
734 // 1 - Item-type - 21H
735 writer
736 .write_u8(0x21)
737 .context(WriteFieldSnafu { field: "Item-type" })?;
738
739 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
740 // value when received.
741 writer
742 .write_u8(0x00)
743 .context(WriteReservedSnafu { bytes: 1_u32 })?;
744
745 write_chunk_u16(writer, |writer| {
746 // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers
747 // between 1 and 255, encoded as an unsigned binary number. For a complete description of
748 // the use of this field see Section 7.1.1.13.
749 writer
750 .write_u8(presentation_context.id)
751 .context(WriteFieldSnafu {
752 field: "Presentation-context-ID",
753 })?;
754
755 // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
756 // value when received.
757 writer
758 .write_u8(0x00)
759 .context(WriteReservedSnafu { bytes: 1_u32 })?;
760
761 // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded as an
762 // unsigned binary number. One of the following values shall be used:
763 // 0 - acceptance
764 // 1 - user-rejection
765 // 2 - no-reason (provider rejection)
766 // 3 - abstract-syntax-not-supported (provider rejection)
767 // 4 - transfer-syntaxes-not-supported (provider rejection)
768 writer
769 .write_u8(match &presentation_context.reason {
770 PresentationContextResultReason::Acceptance => 0,
771 PresentationContextResultReason::UserRejection => 1,
772 PresentationContextResultReason::NoReason => 2,
773 PresentationContextResultReason::AbstractSyntaxNotSupported => 3,
774 PresentationContextResultReason::TransferSyntaxesNotSupported => 4,
775 })
776 .context(WriteFieldSnafu {
777 field: "Presentation Context Result/Reason",
778 })?;
779
780 // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
781 // value when received.
782 writer
783 .write_u8(0x00)
784 .context(WriteReservedSnafu { bytes: 1_u32 })?;
785
786 // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer Syntax
787 // Sub-Item. When the Result/Reason field has a value other than acceptance (0), this field
788 // shall not be significant and its value shall not be tested when received. For a complete
789 // description of the use and encoding of this item see Section 9.3.3.2.1.
790
791 // 1 - Item-type - 40H
792 writer
793 .write_u8(0x40)
794 .context(WriteFieldSnafu { field: "Item-type" })?;
795
796 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
797 // value when received.
798 writer
799 .write_u8(0x40)
800 .context(WriteReservedSnafu { bytes: 1_u32 })?;
801
802 write_chunk_u16(writer, |writer| {
803 // 5-xxx - Transfer-syntax-name - This variable field shall contain the
804 // Transfer-syntax-name proposed for this presentation context. A valid
805 // Transfer-syntax-name shall be encoded as defined in Annex F. For a description of the
806 // use of this field see Section 7.1.1.14. Transfer-syntax-names are structured as UIDs
807 // as defined in PS3.5 (see Annex B for an overview of this concept). DICOM
808 // Transfer-syntax-names are registered in PS3.5.
809 writer
810 .write_all(
811 &codec
812 .encode(&presentation_context.transfer_syntax)
813 .context(EncodeFieldSnafu {
814 field: "Transfer-syntax-name",
815 })?,
816 )
817 .context(WriteFieldSnafu {
818 field: "Transfer-syntax-name",
819 })?;
820
821 Ok(())
822 })
823 .context(WriteChunkSnafu {
824 name: "Transfer Syntax sub-item",
825 })?;
826
827 Ok(())
828 })
829 .context(WriteChunkSnafu {
830 name: "Presentation-context",
831 })
832}
833
834fn write_pdu_variable_user_variables(
835 writer: &mut dyn Write,
836 user_variables: &[UserVariableItem],
837 codec: &dyn TextCodec,
838) -> Result<()> {
839 if user_variables.is_empty() {
840 return Ok(());
841 }
842
843 // 1 - Item-type - 50H
844 writer
845 .write_u8(0x50)
846 .context(WriteFieldSnafu { field: "Item-type" })?;
847
848 // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
849 // value when received.
850 writer
851 .write_u8(0x00)
852 .context(WriteReservedSnafu { bytes: 1_u32 })?;
853
854 write_chunk_u16(writer, |writer| {
855 // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined by
856 // the DICOM Application Entity. The structure and content of these sub-items is defined in
857 // Annex D.
858 for user_variable in user_variables {
859 match user_variable {
860 UserVariableItem::MaxLength(max_length) => {
861 // 1 - Item-type - 51H
862 writer
863 .write_u8(0x51)
864 .context(WriteFieldSnafu { field: "Item-type" })?;
865
866 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
867 // tested to this value when received.
868 writer
869 .write_u8(0x00)
870 .context(WriteReservedSnafu { bytes: 1_u32 })?;
871
872 write_chunk_u16(writer, |writer| {
873 // 5-8 - Maximum-length-received - This parameter allows the
874 // association-requestor to restrict the maximum length of the variable
875 // field of the P-DATA-TF PDUs sent by the acceptor on the association once
876 // established. This length value is indicated as a number of bytes encoded
877 // as an unsigned binary number. The value of (0) indicates that no maximum
878 // length is specified. This maximum length value shall never be exceeded by
879 // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs
880 // received by the association-requestor. Otherwise, it shall be a protocol
881 // error.
882 writer
883 .write_u32::<BigEndian>(*max_length)
884 .context(WriteFieldSnafu {
885 field: "Maximum-length-received",
886 })
887 })
888 .context(WriteChunkSnafu {
889 name: "Maximum-length-received",
890 })?;
891 }
892 UserVariableItem::ImplementationVersionName(implementation_version_name) => {
893 // 1 - Item-type - 55H
894 writer
895 .write_u8(0x55)
896 .context(WriteFieldSnafu { field: "Item-type" })?;
897
898 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
899 // tested to this value when received.
900 writer
901 .write_u8(0x00)
902 .context(WriteReservedSnafu { bytes: 1_u32 })?;
903
904 write_chunk_u16(writer, |writer| {
905 // 5 - xxx - Implementation-version-name - This variable field shall contain
906 // the Implementation-version-name of the Association-acceptor as defined in
907 // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990
908 // (basic G0 set) characters.
909 writer
910 .write_all(&codec.encode(implementation_version_name).context(
911 EncodeFieldSnafu {
912 field: "Implementation-version-name",
913 },
914 )?)
915 .context(WriteFieldSnafu {
916 field: "Implementation-version-name",
917 })
918 })
919 .context(WriteChunkSnafu {
920 name: "Implementation-version-name",
921 })?;
922 }
923 UserVariableItem::ImplementationClassUID(implementation_class_uid) => {
924 // 1 - Item-type - 52H
925 writer
926 .write_u8(0x52)
927 .context(WriteFieldSnafu { field: "Item-type" })?;
928
929 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
930 // tested to this value when received.
931 writer
932 .write_u8(0x00)
933 .context(WriteReservedSnafu { bytes: 1_u32 })?;
934
935 write_chunk_u16(writer, |writer| {
936 //5 - xxx - Implementation-class-uid - This variable field shall contain
937 // the Implementation-class-uid of the Association-acceptor as defined in
938 // Section D.3.3.2. The Implementation-class-uid field is structured as a
939 // UID as defined in PS3.5.
940 writer
941 .write_all(&codec.encode(implementation_class_uid).context(
942 EncodeFieldSnafu {
943 field: "Implementation-class-uid",
944 },
945 )?)
946 .context(WriteFieldSnafu {
947 field: "Implementation-class-uid",
948 })
949 })
950 .context(WriteChunkSnafu {
951 name: "Implementation-class-uid",
952 })?;
953 }
954 UserVariableItem::SopClassExtendedNegotiationSubItem(sop_class_uid, data) => {
955 // 1 - Item-type - 56H
956 writer
957 .write_u8(0x56)
958 .context(WriteFieldSnafu { field: "Item-type" })?;
959 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
960 // tested to this value when received.
961 writer
962 .write_u8(0x00)
963 .context(WriteReservedSnafu { bytes: 1_u32 })?;
964
965 write_chunk_u16(writer, |writer| {
966 write_chunk_u16(writer, |writer| {
967 // 7-xxx - The SOP Class or Meta SOP Class identifier encoded as a UID
968 // as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5.
969 writer
970 .write_all(&codec.encode(sop_class_uid).context(
971 EncodeFieldSnafu {
972 field: "SOP-class-uid",
973 },
974 )?)
975 .context(WriteFieldSnafu {
976 field: "SOP-class-uid",
977 })
978 })
979 .context(WriteChunkSnafu {
980 name: "SOP-class-uid",
981 })?;
982
983 // xxx-xxx Service-class-application-information - This field shall contain
984 // the application information specific to the Service Class specification
985 // identified by the SOP-class-uid. The semantics and value of this field is
986 // defined in the identified Service Class specification.
987 writer.write_all(data).context(WriteFieldSnafu {
988 field: "Service-class-application-information",
989 })
990 })
991 .context(WriteChunkSnafu { name: "Sub-item" })?;
992 }
993 UserVariableItem::UserIdentityItem(user_identity) => {
994 // 1 - Item-type - 58H
995 writer
996 .write_u8(0x58)
997 .context(WriteFieldSnafu { field: "Item-type" })?;
998
999 // 2 - Reserved - This reserved field shall be sent with a value 00H but not
1000 // tested to this value when received.
1001 writer
1002 .write_u8(0x00)
1003 .context(WriteReservedSnafu { bytes: 1_u32 })?;
1004
1005 // 3-4 - Item-length
1006 write_chunk_u16(writer, |writer| {
1007 // 5 - User-Identity-Type
1008 writer
1009 .write_u8(user_identity.identity_type().to_u8())
1010 .context(WriteFieldSnafu {
1011 field: "User-Identity-Type",
1012 })?;
1013
1014 // 6 - Positive-response-requested
1015 let positive_response_requested_out: u8 =
1016 if user_identity.positive_response_requested() {
1017 1
1018 } else {
1019 0
1020 };
1021 writer.write_u8(positive_response_requested_out).context(
1022 WriteFieldSnafu {
1023 field: "Positive-response-requested",
1024 },
1025 )?;
1026
1027 // 7-8 - Primary-field-length
1028 write_chunk_u16(writer, |writer| {
1029 // 9-n - Primary-field
1030 writer
1031 .write_all(user_identity.primary_field().as_slice())
1032 .context(WriteFieldSnafu {
1033 field: "Primary-field",
1034 })
1035 })
1036 .context(WriteChunkSnafu {
1037 name: "Primary-field",
1038 })?;
1039
1040 // n+1-n+2 - Secondary-field-length
1041 write_chunk_u16(writer, |writer| {
1042 // n+3-m - Secondary-field
1043 writer
1044 .write_all(user_identity.secondary_field().as_slice())
1045 .context(WriteFieldSnafu {
1046 field: "Secondary-field",
1047 })
1048 })
1049 .context(WriteChunkSnafu {
1050 name: "Secondary-field",
1051 })
1052 })
1053 .context(WriteChunkSnafu {
1054 name: "Item-length",
1055 })?;
1056 }
1057 UserVariableItem::Unknown(item_type, data) => {
1058 writer
1059 .write_u8(*item_type)
1060 .context(WriteFieldSnafu { field: "Item-type" })?;
1061
1062 writer
1063 .write_u8(0x00)
1064 .context(WriteReservedSnafu { bytes: 1_u32 })?;
1065
1066 write_chunk_u16(writer, |writer| {
1067 writer.write_all(data).context(WriteFieldSnafu {
1068 field: "Unknown Data",
1069 })
1070 })
1071 .context(WriteChunkSnafu { name: "Unknown" })?;
1072 }
1073 }
1074 }
1075
1076 Ok(())
1077 })
1078 .context(WriteChunkSnafu { name: "User-data" })
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084 use std::io::Cursor;
1085
1086 #[test]
1087 fn can_write_chunks_with_preceding_u32_length() -> Result<()> {
1088 let mut bytes = vec![0u8; 0];
1089 write_chunk_u32(&mut bytes, |writer| {
1090 writer
1091 .write_u8(0x02)
1092 .context(WriteFieldSnafu { field: "Field1" })?;
1093 write_chunk_u32(writer, |writer| {
1094 writer
1095 .write_u8(0x03)
1096 .context(WriteFieldSnafu { field: "Field2" })?;
1097 Ok(())
1098 })
1099 .context(WriteChunkSnafu { name: "Chunk2" })
1100 })
1101 .context(WriteChunkSnafu { name: "Chunk1" })?;
1102
1103 assert_eq!(bytes.len(), 10);
1104 assert_eq!(bytes, &[0, 0, 0, 6, 2, 0, 0, 0, 1, 3]);
1105
1106 Ok(())
1107 }
1108
1109 #[test]
1110 fn can_write_chunks_with_preceding_u16_length() -> Result<()> {
1111 let mut bytes = vec![0u8; 0];
1112 write_chunk_u16(&mut bytes, |writer| {
1113 writer
1114 .write_u8(0x02)
1115 .context(WriteFieldSnafu { field: "Field1" })?;
1116 write_chunk_u16(writer, |writer| {
1117 writer
1118 .write_u8(0x03)
1119 .context(WriteFieldSnafu { field: "Field2" })?;
1120 Ok(())
1121 })
1122 .context(WriteChunkSnafu { name: "Chunk2" })
1123 })
1124 .context(WriteChunkSnafu { name: "Chunk1" })?;
1125
1126 assert_eq!(bytes.len(), 6);
1127 assert_eq!(bytes, &[0, 4, 2, 0, 1, 3]);
1128
1129 Ok(())
1130 }
1131
1132 #[test]
1133 fn write_abort_rq() {
1134 let mut out = vec![];
1135
1136 // abort by request of SCU
1137 let pdu = Pdu::AbortRQ {
1138 source: AbortRQSource::ServiceUser,
1139 };
1140 write_pdu(&mut out, &pdu).unwrap();
1141 assert_eq!(
1142 &out,
1143 &[
1144 // code 7 + reserved byte
1145 0x07, 0x00, //
1146 // PDU length: 4 bytes
1147 0x00, 0x00, 0x00, 0x04, //
1148 // reserved 2 bytes + source: service user (0) + reason (0)
1149 0x00, 0x00, 0x00, 0x00,
1150 ]
1151 );
1152 out.clear();
1153
1154 // Reserved
1155 let pdu = Pdu::AbortRQ {
1156 source: AbortRQSource::Reserved,
1157 };
1158 write_pdu(&mut out, &pdu).unwrap();
1159 assert_eq!(
1160 &out,
1161 &[
1162 // code 7 + reserved byte
1163 0x07, 0x00, //
1164 // PDU length: 4 bytes
1165 0x00, 0x00, 0x00, 0x04, //
1166 // reserved 2 bytes + source: reserved (1) + reason (0)
1167 0x00, 0x00, 0x01, 0x00,
1168 ]
1169 );
1170 out.clear();
1171
1172 // abort by request of SCP
1173 let pdu = Pdu::AbortRQ {
1174 source: AbortRQSource::ServiceProvider(
1175 AbortRQServiceProviderReason::InvalidPduParameter,
1176 ),
1177 };
1178 write_pdu(&mut out, &pdu).unwrap();
1179 assert_eq!(
1180 &out,
1181 &[
1182 // code 7 + reserved byte
1183 0x07, 0x00, //
1184 // PDU length: 4 bytes
1185 0x00, 0x00, 0x00, 0x04, //
1186 // reserved 2 bytes
1187 0x00, 0x00, //
1188 // source: service provider (2), invalid parameter value (6)
1189 0x02, 0x06,
1190 ]
1191 );
1192 }
1193
1194 #[test]
1195 fn extended_negotiation_bytestream_roundtrip() -> Result<()> {
1196 let pdu = Pdu::AssociationRQ(AssociationRQ {
1197 protocol_version: 1,
1198 calling_ae_title: "SCU".to_string(),
1199 called_ae_title: "SCP".to_string(),
1200 application_context_name: "1.2.3".to_string(),
1201 presentation_contexts: vec![],
1202 user_variables: vec![UserVariableItem::SopClassExtendedNegotiationSubItem(
1203 "1.2.3.4".to_string(),
1204 vec![1, 0, 1, 1],
1205 )],
1206 });
1207 let mut out = Vec::<u8>::new();
1208
1209 // Serialize and check serialized stream
1210 write_pdu(&mut out, &pdu)?;
1211
1212 #[rustfmt::skip]
1213 assert_eq!(
1214 out,
1215 &[1, 0, // A-ASSOCIATE-RQ PDU type and reserved byte
1216 0, 0, 0, 98, // PDU Total length (Big Endian)
1217 0, 1, // Protocol version bits (BE)
1218 0, 0, // Reserved
1219
1220 // Called AE Title, space-padded
1221 b'S', b'C', b'P', b' ', b' ', b' ', b' ', b' ',
1222 b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ',
1223
1224 // Calling AE Title, space-padded
1225 b'S', b'C', b'U', b' ', b' ', b' ', b' ', b' ',
1226 b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ',
1227
1228 // 32 reserved bytes
1229 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1230 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1231
1232 0x10, 0, // Application Context Name container
1233 0, 5, // Length of Application Context Name (BE)
1234 b'1', b'.', b'2', b'.', b'3', // Application Context Name
1235
1236 0x50, 0, // User Variables container
1237 0, 17, // Total length of User Variables (BE)
1238 0x56, 0, // Extended Negotiation container
1239 0, 13, // Length of bytes contained in E.N. (BE)
1240 0, 7, // Length of SOP Class UID string (BE)
1241 // SOP Class UID string
1242 b'1', b'.', b'2', b'.', b'3', b'.', b'4',
1243 // Service-class-application-information field
1244 1, 0, 1, 1,
1245 ],
1246 );
1247
1248 // Deserialize and check against original A-ASSOCIATE-RQ PDU
1249 // (checks that it round-trips)
1250 let mut buf = Cursor::new(&mut out);
1251 let pdu2 = read_pdu(&mut buf, 16384, false).unwrap().unwrap();
1252 assert_eq!(pdu, pdu2);
1253
1254 Ok(())
1255 }
1256}