Skip to main content

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}